Внутренний CA протух через десять лет и остановил обмен между сервисами
Однажды утром десяток сервисов, которые годами спокойно ходили друг к другу по внутренней сети, разом перестали понимать друг друга. Не по одному, не волной — все сразу, в один и тот же момент. Причина оказалась не в атаке, не в баге деплоя и не в сетевом сбое, а в сертификате, который выписали ещё на старте проекта и о котором с тех пор никто не вспоминал. У него просто закончился срок действия — ровно через десять лет после выпуска.
Содержание
- Как всё началось: сервисы разом перестали видеть друг друга
- Первые гипотезы — и почему они не подтвердились
- Что показали логи: один и тот же timestamp у всех ошибок
- Настоящая причина: root CA, который никто не считал сертификатом
- Почему это не поймали раньше: слепое пятно в мониторинге
- Как разворачивали новый корень без полной остановки сервисов
- Что изменили после: мониторинг всей цепочки, а не только листьев
Как всё началось: сервисы разом перестали видеть друг друга
Инфраструктура была устроена стандартно для разросшегося 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 действительно продлевала листовые сертификаты автоматически и без вмешательства человека, и это создавало иллюзию "у нас с сертификатами всё под контролем, тут автоматика". На деле автоматика покрывала только часть дерева доверия.
Как разворачивали новый корень без полной остановки сервисов
Экстренное восстановление разбилось на несколько шагов, и часть времени ушла именно на то, чтобы сделать это без полного простоя (грубый ориентир по опыту подобных инцидентов — несколько часов от обнаружения до полного восстановления, у вас будет своя цифра в зависимости от числа узлов и того, насколько автоматизирован деплой конфигов):
- Выпустили новый корень и промежуточный сертификат с более разумным сроком действия и сразу зафиксировали процесс ротации в коде, а не в памяти инженеров:
step ca init \
--name "Internal Root CA v2" \
--provisioner admin \
--deployment-type standalone
- Раздали новый trust bundle всем сервисам параллельно со старым. Ключевой трюк — не заменять старый корень новым мгновенно, а какое-то время доверять обоим: обновили конфиги TLS так, чтобы каждый сервис принимал сертификаты, подписанные либо старым (уже неработающим на клиентах, но ещё нужным для части бэкапов и логов), либо новым корнем.
- Перевыпустили все leaf-сертификаты от нового промежуточного через существующий пайплайн step-ca/ACME — благо этот механизм и так работал штатно, поменялся только источник доверия:
step ca certificate "billing.internal" billing.crt billing.key \
--provisioner admin
- Раскатили конфиги и сделали rolling restart сервисов по одному, а не все разом, чтобы не потерять API-шлюз и не блокировать восстановление живой очередью запросов, которые продолжали копиться.
- Проверили, что все девять сервисов видят друг друга прогоном той же связки команд
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 ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →