Сертификаты истекли по расписанию. Алерт неделю кричал — в пустоту
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 ноды, я продлевал их по одной и проверял каждую, прежде чем трогать следующую:
- Бэкап
/etc/kubernetesв архив. kubeadm certs renew allс kubeadm-конфигом кластера. SAN’ы сохраняются.- Убрать манифесты static pod’ов, дождаться, пока API-сервер, controller-manager и scheduler исчезнут, вернуть манифесты.
- Проверить
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 на другом мастере: когда первый рестартует, у тебя остаётся вход.