Боевой сертификат истёк, и никто не заметил: внешний прокси прикрыл аварию собой
Все публичные хосты отдавали 200, пока кластер отдавал просроченный wildcard. Прокси со своей TLS-терминацией спрятал аварию; настоящая находка — отсутствие алерта на истечение.
В Optimacros задача пришла как рутина: «новые сертификаты для кластера с маркетинговыми сайтами», архив с сертификатом, ключом и цепочкой CA. При инвентаризации выяснилось, что сертификат в кластере истёк 49 минут назад — и об этом никто не сообщил. Ни алерт, ни пользователи, ни мониторинг. Все пять публичных хостов отдавали 200.
Почему авария не проявилась
Топология гибридная. Внешний прокси на отдельной VM сам терминирует TLS и ходит в кластерный origin, не проверяя его сертификат. Новый сертификат на этот прокси кто-то раскатал ещё неделей раньше: серийник живого :443 совпал с файлом из архива байт в байт. Снаружи всё выглядело исправным.
Сломан был другой путь — прямой вход в кластерный gateway по SNI. Им ходят health-check’и, мониторинг и всё, что обходит прокси. Для них сервис лежал.
«Сайт отвечает 200» ничего не доказывает про TLS, если перед ним стоит прокси со своей терминацией. Проверять надо тот путь, который чинишь, а не тот, который удобно проверить:
echo | openssl s_client -connect <gateway-ip>:443 -servername <host> \
| openssl x509 -noout -serial -enddate
Чинить в источнике
Секреты в Kubernetes управлялись Vault Secrets Operator, так что kubectl apply затёрло бы правку в течение пяти минут. Сертификат нужно было положить в Vault, а раскатал бы его оператор.
До записи я проверил материал: modulus ключа совпал с сертификатом, openssl verify прошёл по цепочке CA. Потом бэкап текущих версий в Vault, потом запись — полной цепочки, а не только leaf. Локальный openssl говорит OK и для одного leaf, потому что промежуточный сертификат уже лежит в сторе рабочей станции, а мобильные клиенты и минимальные образы падают с unable to get local issuer certificate. Проверка, которая имеет значение, — curl -w '%{ssl_verify_result}' с чистого клиента.
Копий секрета было пять, а на listener gateway ссылалась только одна — остальные остались от прошлой миграции. Обновил все, чтобы устаревшие объекты не сбивали следующий разбор. Итого около 15 минут, без простоя.
Грабли по дороге
- Сменился выпускающий CA. Ничего не сломалось только потому, что сертификат origin никто не проверял. Любая будущая backend-TLS-политика превратила бы ту же замену в аварию.
- Мой
KUBECONFIGбыл прибит к другому кластеру. Первая жеkubectl get nodesпоказала чужие ноды, и корпоративный wildcard едва не уехал не туда. Перед любой мутацией сверяй, в каком ты кластере.
Выводы
- Настоящая корневая причина — не поздняя замена, а отсутствие алерта на истечение сертификатов. Blackbox-проба по
probe_ssl_earliest_cert_expiryзакрывает весь флот разом. - Прокси с собственной TLS-терминацией способен спрятать за собой мёртвый сертификат. Проверяй каждый вход отдельно.
- Правь секреты там, где их источник правды, иначе оператор откатит твою правку.
- Всегда клади полную цепочку и проверяй клиентом без лишних промежуточных сертификатов.