← Все статьи

Сертификаты истекли по расписанию. Алерт неделю кричал — в пустоту

kubeadm-кластер потерял control plane ровно через год после установки. Алерт об истечении горел семь дней, но у Alertmanager был только receiver null.

Однажды утром каждый вызов kubectl к bare-metal кластеру в Optimacros падал с одной и той же строкой:

x509: certificate has expired or is not yet valid: current time ... is after 2026-10-09T04:52:15Z

Работающие поды продолжали обслуживать трафик, так что снаружи ничего не выглядело сломанным. Внутри controller-manager и scheduler крутились в крашлупе — по 28–31 рестарту. Ничего нового не шедулилось, ничего не реконсилилось. CI-джобы, которым нужны свежие поды, просто падали.

Ровно год

Дата всё выдала. Кластер поставили kubespray’ем 2025-10-09 и с тех пор ни разу не апгрейдили. kubeadm выпускает листовые сертификаты — API-сервера, клиента kubelet, front-proxy и kubeconfig’и admin, controller-manager и scheduler — на один год. kubeadm upgrade попутно их продлевает, поэтому регулярно обновляемые кластеры этого не замечают. Этот никто не трогал, а auto_renew_certificates у kubespray был выключен. Истекли все в одну секунду.

Не менее важно для починки то, что не пострадало: CA живёт десять лет, etcd-сертификаты kubespray — сто, клиентские сертификаты kubelet ротируются сами. Значит, это продление, а не пересборка.

Алерт сработал

Первая мысль была «у нас не было алерта на это». Она оказалась неверной, а правда хуже. kube-prometheus-stack из коробки содержит KubeClientCertificateExpiration. Я посмотрел его историю в Prometheus:

count_over_time(ALERTS{alertname="KubeClientCertificateExpiration",alertstate="firing"}[10d])

Он горел warning’ом около семи дней и critical’ом последние сутки. Потом я открыл сгенерированный конфиг Alertmanager. Единственный receiver — "null": дефолт чарта, потому что Argo CD Application так и не задал alertmanager.config. Алерт работал идеально. Ему просто некуда было идти.

Починка — по одному мастеру

В кластере три control-plane ноды, я продлевал их по одной и проверял каждую, прежде чем трогать следующую:

  1. Бэкап /etc/kubernetes в архив.
  2. kubeadm certs renew all с kubeadm-конфигом кластера. SAN’ы сохраняются.
  3. Убрать манифесты static pod’ов, дождаться, пока API-сервер, controller-manager и scheduler исчезнут, вернуть манифесты.
  4. Проверить readyz и, главное, сертификат, который реально отдаётся:
echo | openssl s_client -connect <master>:6443 2>/dev/null | openssl x509 -noout -enddate

Шаг 3 обычно пропускают. kubeadm certs renew только пишет файлы. Запущенные процессы держат старый сертификат в памяти, пока static pod’ы не перезапустятся, так что «файлы новые» ничего не доказывает.

kubeconfig на ноутбуке тоже пришлось заменить новым admin.conf. И ещё грабля: kubespray прописывает server: на 127.0.0.1:6443, поэтому скопированный файл даёт connection refused, пока не перепишешь адрес.

Чтобы не повторилось

Профилактика: ежемесячный systemd-таймер, который продлевает сертификаты, — клон того, что kubespray поставил бы с auto_renew_certificates: true, с разнесением по мастерам, чтобы они никогда не рестартовали одновременно. Я один раз запустил его руками и снова проверил отдаваемый сертификат: таймер, успеха которого ты не видел, — это надежда, а не контроль.

Настоящая дыра — в доставке, и она шире этого кластера: алерт без receiver’а — декорация. Подключение доставки Alertmanager через GitOps записано отдельным решением со своим триггером.

Выводы

  • kubeadm-кластер, который не апгрейдят хотя бы раз в год, потеряет control plane в годовщину установки.
  • После продления сертификатов проверяй, что отдаёт эндпоинт, а не что лежит на диске.
  • «У нас не было алерта» и «алерт ушёл в никуда» во время инцидента выглядят одинаково. Проверь receivers на каждом кластере — одинокий null значит, что никто не слушает.
  • Держи отдельный admin-kubeconfig на другом мастере: когда первый рестартует, у тебя остаётся вход.