MAATRIX / Блог / Внутренний CA протух через десять лет и остановил обмен между сервисами

Внутренний CA протух через десять лет и остановил обмен между сервисами

MAATRIX

Однажды утром десяток сервисов, которые годами спокойно ходили друг к другу по внутренней сети, разом перестали понимать друг друга. Не по одному, не волной — все сразу, в один и тот же момент. Причина оказалась не в атаке, не в баге деплоя и не в сетевом сбое, а в сертификате, который выписали ещё на старте проекта и о котором с тех пор никто не вспоминал. У него просто закончился срок действия — ровно через десять лет после выпуска.

Как всё началось: сервисы разом перестали видеть друг друга

Инфраструктура была устроена стандартно для разросшегося backend на выделенных серверах: десяток внутренних сервисов (биллинг, очередь задач, API-шлюз, воркеры, сервис уведомлений) общались между собой не по голому HTTP, а по mTLS — каждый сервис предъявлял собеседнику клиентский сертификат, и обе стороны проверяли друг друга по общему внутреннему корню доверия (root CA), который был поднят один раз при запуске проекта.

Утром мониторинг начал сыпать алертами: growing error rate на API-шлюзе, таймауты у воркеров, очередь задач перестала разбираться. Внешне это выглядело как классический каскадный отказ — будто один сервис лёг и утянул за собой остальных (мы разбирали похожий сценарий в статье про то, как один упавший микросервис утянул за собой ещё пять). Но в этот раз картина была другой: не было единой точки отказа. Абсолютно все сервисы одновременно жаловались друг на друга — каждый TLS-хендшейк между любой парой процессов в кластере обрывался с одной и той же ошибкой:

x509: certificate has expired or is not yet valid: current time 2026-08-27T06:14:02Z is after 2026-08-27T06:00:00Z

Единственное, что связывало все инциденты — точное время. Секунда в секунду.

Первые гипотезы — и почему они не подтвердились

Первая мысль дежурного инженера: рассинхронизация времени на серверах, из-за которой рвётся TLS. Проверили chronyc tracking на всех нодах — офсет в пределах пары миллисекунд, NTP работал штатно. Гипотеза отпала за пять минут.

Вторая гипотеза — проблема с сетью или с балансировщиком: может, кто-то поменял правила firewall или откатил конфиг nginx перед сервисами. Посмотрели историю деплоев — последний релиз был три дня назад и трогал только один сервис уведомлений, остальные девять не деплоились неделями. Не билось по времени: все сервисы упали в одну секунду, а деплой был давно.

Третья гипотеза — DNS. Раз в полгода у нас случается что-то похожее на кеш отрицательных ответов или на резолвер, уходящий в таймаут после смены ключей DNSSEC. Проверили резолвинг внутренних имён через dig — всё резолвилось мгновенно и правильно. DNS был ни при чём.

Четвёртая гипотеза, уже ближе к истине — истёк внешний SSL-сертификат Let's Encrypt на публичном домене. Открыли openssl s_client -connect app.example.com:443 — сертификат свежий, до истечения ещё 83 дня, обновлён cron-джобом certbot штатно неделю назад. Внешний трафик вообще не был затронут: сайт открывался, платежи проходили. Ломалось строго внутреннее общение между сервисами.

Только после того, как отбросили четыре версии подряд, кто-то заметил в логе не просто "certificate expired", а конкретную дату истечения — и она была одинаковой у всех сертификатов сразу, у всех сервисов, без исключения.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Что показали логи: один и тот же timestamp у всех ошибок

Собрали ошибки TLS-хендшейка со всех сервисов за последний час и прогнали через openssl x509 -noout -enddate по каждому сертификату из цепочки, которую предъявлял каждый сервис:

for host in billing queue gateway worker notifier; do
  echo "== $host =="
  openssl s_client -connect ${host}.internal:8443 -showcerts </dev/null 2>/dev/null \
    | openssl x509 -noout -subject -enddate
done

Результат был одинаковым для всех пяти сервисов:

subject=CN = billing.internal
notAfter=Aug 27 06:00:00 2026 GMT

Разные CN, разные сервисы, разные команды, которые их писали и деплоили в разное время за последние три года — но у всех notAfter указывал на одну и ту же секунду. Это означало одно: все leaf-сертификаты были подписаны одним и тем же промежуточным сертификатом, а срок действия им всем ограничивал не собственный TTL, а истечение вышестоящего звена цепочки.

Поднялись по цепочке до самого верха:

openssl x509 -in /etc/pki/ca/root-ca.crt -noout -subject -issuer -enddate
subject=CN = Internal Root CA
issuer=CN = Internal Root CA
notAfter=Aug 27 06:00:00 2026 GMT

Вот он. Самоподписанный корневой сертификат, выпущенный самим на себя, с точным совпадением даты — до секунды — с моментом, когда всё рухнуло.

Настоящая причина: root CA, который никто не считал сертификатом

Раскопали git-историю каталога с инфраструктурными скриптами и нашли коммит десятилетней давности — тот самый, где кто-то из первой команды поднимал внутреннюю PKI для mTLS между будущими микросервисами. Команда генерации корня выглядела примерно так:

openssl genrsa -out root-ca.key 4096
openssl req -x509 -new -nodes -key root-ca.key \
  -sha256 -days 3650 -out root-ca.crt \
  -subj "/CN=Internal Root CA"

-days 3650 — ровно десять лет. В момент, когда это писалось, десять лет казались бесконечностью: проект был молодой, о таком горизонте планирования никто всерьёз не думал. Дальше от этого корня подписали один промежуточный сертификат, а от промежуточного — уже конечные (leaf) сертификаты для каждого сервиса, которые с тех пор регулярно перевыпускались автоматикой (сначала самописным скриптом на cron, потом step-ca с коротким TTL в 90 дней).

Здесь и была ловушка: leaf-сертификаты обновлялись исправно, каждые три месяца, без единого сбоя за годы — поэтому никому и в голову не приходило проверять что-то ещё в этой цепочке. А промежуточный и корневой сертификаты были подписаны один раз при создании и с тех пор к ним никто не прикасался. Формально TLS-цепочка валидна ровно до тех пор, пока валидно самое слабое звено — а слабым звеном оказался не тот сертификат, который все привыкли считать "тем самым, который надо продлевать".

Почему это не поймали раньше: слепое пятно в мониторинге

У компании был настроен мониторинг сертификатов — прямо по мотивам того, что описано в статье про мониторинг сертификатов и доменов: blackbox-exporter проверял notAfter у публичного домена, алерты приходили за 30 и за 14 дней до истечения внешнего SSL. Но эта проверка ходила только на 443-й порт публичного HTTP-эндпоинта. Она даже не подозревала о существовании внутреннего root CA — тот вообще никогда не участвовал в сетевом обмене напрямую, он тихо лежал файлом на диске центра выдачи сертификатов (step-ca) и в доверенных хранилищах (/etc/ssl/certs, trust bundle в конфигах каждого сервиса) на всех нодах.

Второй фактор — текучка команды. Инженеры, которые поднимали PKI и понимали, что где-то в основании всей конструкции лежит сертификат с TTL в десять лет, к моменту инцидента давно не работали в компании. Знание не было задокументировано нигде, кроме старого README в приватном репозитории, который никто не открывал годами. Формально это классическая "правда справедливая только пока жив автор" — устная договорённость, которая должна была стать пунктом в runbook, но не стала.

Третий фактор — ложное чувство защищённости от самого механизма автопродления. Renewal-джоба step-ca действительно продлевала листовые сертификаты автоматически и без вмешательства человека, и это создавало иллюзию "у нас с сертификатами всё под контролем, тут автоматика". На деле автоматика покрывала только часть дерева доверия.

Как разворачивали новый корень без полной остановки сервисов

Экстренное восстановление разбилось на несколько шагов, и часть времени ушла именно на то, чтобы сделать это без полного простоя (грубый ориентир по опыту подобных инцидентов — несколько часов от обнаружения до полного восстановления, у вас будет своя цифра в зависимости от числа узлов и того, насколько автоматизирован деплой конфигов):

  1. Выпустили новый корень и промежуточный сертификат с более разумным сроком действия и сразу зафиксировали процесс ротации в коде, а не в памяти инженеров:
step ca init \
  --name "Internal Root CA v2" \
  --provisioner admin \
  --deployment-type standalone
  1. Раздали новый trust bundle всем сервисам параллельно со старым. Ключевой трюк — не заменять старый корень новым мгновенно, а какое-то время доверять обоим: обновили конфиги TLS так, чтобы каждый сервис принимал сертификаты, подписанные либо старым (уже неработающим на клиентах, но ещё нужным для части бэкапов и логов), либо новым корнем.
  1. Перевыпустили все leaf-сертификаты от нового промежуточного через существующий пайплайн step-ca/ACME — благо этот механизм и так работал штатно, поменялся только источник доверия:
step ca certificate "billing.internal" billing.crt billing.key \
  --provisioner admin
  1. Раскатили конфиги и сделали rolling restart сервисов по одному, а не все разом, чтобы не потерять API-шлюз и не блокировать восстановление живой очередью запросов, которые продолжали копиться.
  1. Проверили, что все девять сервисов видят друг друга прогоном той же связки команд openssl s_client + x509 -noout -enddate, что использовали для диагностики, только теперь ожидая дату на десять лет вперёд, а не позади текущего момента.

Отдельно стоит сказать: если бы инфраструктура строилась заново сегодня, разумнее было бы с самого начала не городить самодельный root CA руками через openssl req -x509, а взять управляемое решение вроде step-ca с изначально коротким TTL и встроенной ротацией промежуточных сертификатов, либо PKI-backend HashiCorp Vault, где срок действия каждого звена виден в одном месте и ротация корня — штатная, документированная операция, а не разовый скрипт десятилетней давности.

Что изменили после: мониторинг всей цепочки, а не только листьев

Главный вывод из инцидента — сертификат перестаёт быть проблемой не тогда, когда он "автоматически обновляется", а тогда, когда весь путь от листа до корня виден мониторингу и укладывается в разумный горизонт планирования. После разбора внедрили следующее:

Что проверяли до инцидентаЧто проверяем после
Только внешний домен на 443Каждый сертификат в цепочке: leaf, intermediate, root
Только через публичный blackbox-запросЛокальный скрипт на каждой ноде читает файлы сертификатов напрямую с диска
Алерт за 30/14 днейАлерты за 90/30/7 дней, эскалация при попадании root CA в окно 6 месяцев
Ручное знание "кто и как поднимал PKI"Runbook в репозитории: как выпустить новый корень, как раздать trust bundle, как делать rolling restart
TTL root CA — 10 лет "и забыли"TTL root CA — 3 года, с календарным напоминанием на ротацию за год до истечения

Скрипт мониторинга теперь обходит все узлы и для каждого файла сертификата в доверенном хранилище считает разницу между notAfter и текущей датой, а не полагается на то, что сервис сам пожалуется на истечение через сетевую ошибку:

#!/usr/bin/env bash
for cert in /etc/pki/ca/*.crt /etc/ssl/certs/internal-*.pem; do
  end=$(openssl x509 -enddate -noout -in "$cert" | cut -d= -f2)
  end_ts=$(date -d "$end" +%s)
  now_ts=$(date +%s)
  days_left=$(( (end_ts - now_ts) / 86400 ))
  if [ "$days_left" -lt 90 ]; then
    echo "WARNING: $cert истекает через $days_left дней"
  fi
done

Этот скрипт запустили как отдельную cron-задачу с отправкой алерта в тот же канал, куда падают остальные инфраструктурные уведомления — идея та же, что и в общем подходе к мониторингу сертификатов и доменов, но с прицелом именно на внутреннюю PKI, а не только на публичные эндпоинты.

Отдельно завели правило: любой сертификат с TTL больше двух лет требует отдельного review и записи в runbook с датой следующей проверки — просто потому что горизонт "напишу — и через пару месяцев само продлится" здесь не работает, а горизонт "через десять лет кто-нибудь разберётся" на практике означает "никто не разберётся, пока не рухнет прод".

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Почему leaf-сертификаты обновлялись автоматически, а root CA — нет?

Потому что автоматика (step-ca/ACME) была настроена именно для листовых сертификатов, которые нужно продлевать часто — раз в 90 дней. Root CA создаётся один раз и живёт годами, поэтому для него никто не настраивал отдельный процесс ротации — его просто не воспринимали как объект, требующий регулярного внимания.

Можно ли было продлить старый root CA вместо выпуска нового?

Нет: у самоподписанного корневого сертификата нет вышестоящего центра, который мог бы его "продлить" — по истечении notAfter сертификат считается недействительным окончательно, и единственный выход — сгенерировать новый ключ и новый сертификат, а затем распространить новый trust bundle на все узлы, которые ему доверяли.

Как заранее понять, что у нас есть подобный риск в инфраструктуре?

Пройдитесь по всем сертификатам, которые лежат в доверенных хранилищах на серверах (/etc/ssl/certs, /etc/pki/, конфиги TLS в приложениях), и явно проверьте notAfter у каждого, включая корневые и промежуточные — не только у тех, что видит внешний мониторинг сайта.

Стоит ли вообще поднимать свой root CA для внутреннего mTLS, или лучше готовое решение?

Готовые инструменты вроде step-ca или PKI-движка Vault не устраняют саму идею конечного TTL, но избавляют от необходимости писать и поддерживать логику выпуска и ротации руками — а именно ручная, никем не сопровождаемая часть чаще всего и превращается в подобный инцидент спустя годы.

Почему все сервисы упали одновременно, а не по одному по мере истечения их сертификатов?

Потому что TTL каждого конкретного сервиса истёк бы позже (leaf-сертификаты продлевались каждые 90 дней), но валидность всей цепочки ограничена самым коротким по оставшемуся сроку звеном — а им внезапно оказался не лист, а корень.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →