{"id":"IL-0183","org":"ckck803","date":"2026-02-22","docs":[],"title":{"en":"kubeadm's one-year leaf certificates expired, so kube-apiserver could not reach etcd, restarted in a loop and left the cluster unreachable","ko":"kubeadm 으로 만든 클러스터의 1년짜리 인증서가 만료되어 kube-apiserver 가 etcd 와 TLS 연결을 맺지 못하고 재시작을 반복했고, 클러스터에 접속할 수 없게 됐다."},"domain":["kubernetes"],"impact":{"scope":"The whole cluster: the API server was down and the cluster could not be reached from outside","users":"unknown","region":"unknown"},"review":{"written_by":"session","reviewed_at":null,"reviewed_by":null},"lessons":{"en":"A kubeadm cluster stops working one year after its leaf certificates were issued unless they are renewed. The first errors seen may be TLS handshake failures or a flag error that does not mention certificates; the decisive message is the x509 expiry in the kube-apiserver log for its connection to etcd.\n\nThe author recommends checking expiry with kubeadm certs check-expiration and renewing from 30 days before. kubeadm upgrade apply renews the certificates too, so a cluster upgraded at least once a year avoids the problem; a cluster run for a long time without upgrades can renew from cron, for example with 0 3 1 1 * root kubeadm certs renew all && systemctl restart kubelet in /etc/cron.d/kubeadm-cert-renew.","ko":"sudo kubeadm certs check-expiration 으로 만료일을 주기적으로 확인하고, 만료 30일 전부터 갱신하는 것이 좋다. kubeadm upgrade apply 를 실행하면 인증서도 함께 갱신되므로, 1년에 한 번 이상 쿠버네티스 버전을 올리는 클러스터는 이 문제를 자연스럽게 피한다. 버전을 올리지 않고 오래 운영하는 클러스터라면 kubeadm certs renew all 과 kubelet 재시작을 cron 으로 매년 걸어 둘 수 있다. 작성자가 든 예시에서는 매년 1월 1일 새벽 3시에 실행한다."},"related":[],"sources":[{"url":"https://ckck803.github.io/2026/02/22/k8s/trouble/certification/","kind":"blog","license":"unknown","archived":null}],"summary":{"en":"A cluster built with kubeadm uses TLS certificates that kubeadm generates for internal communication. The CA certificates are valid for 10 years, but the leaf certificates are valid for one year by default and must be renewed every year. On this cluster all eleven leaf certificates expired at 2026-02-20 16:16 UTC. kube-apiserver could no longer complete the TLS handshake with etcd and exited every time it started, and without an API server the kubelet could not register its node and Calico could not tear down pod networks.\n\nThe author renewed all certificates with sudo kubeadm certs renew all. Because the cluster was completely down, kubeadm printed \"Error reading configuration from the Cluster. Falling back to default configuration\" and renewed with its defaults. kube-apiserver, kube-controller-manager, kube-scheduler and etcd run as static pods, so restarting containerd and the kubelet restarted them with the new certificates. The renewed /etc/kubernetes/admin.conf was then copied to $HOME/.kube/config, and commands such as kubectl get nodes were checked to work.","ko":"kubeadm 으로 구성한 클러스터는 내부 통신에 쓰는 TLS 인증서를 자동으로 만든다. 이 인증서의 기본 유효기간은 1년이고, CA 인증서는 10년이다. 그래서 해마다 하위 인증서만 갱신해야 하는데, 이 시점을 놓치면 클러스터에 갑자기 접속할 수 없게 된다. 작성자의 클러스터에서는 하위 인증서가 2026-02-20 16:16 UTC 에 모두 만료됐다. kube-apiserver 는 만료된 인증서로 etcd 와 TLS 연결을 맺지 못해 시작하자마자 죽기를 반복했다. API 서버가 없으니 kubelet 은 노드 등록과 lease 갱신에 실패했고, Calico 도 API 서버에 닿지 못했다.\n\nsudo kubeadm certs check-expiration 으로 보니 admin.conf, apiserver, apiserver-etcd-client, apiserver-kubelet-client, controller-manager.conf, etcd-healthcheck-client, etcd-peer, etcd-server, front-proxy-client, scheduler.conf, super-admin.conf 의 RESIDUAL TIME 이 모두 <invalid> 였다. CA 인증서인 ca, etcd-ca, front-proxy-ca 는 2035-02-18 까지 유효했다. 작성자는 sudo kubeadm certs renew all 로 인증서를 한 번에 갱신했다. 클러스터가 완전히 멈춰 있어서 Error reading configuration from the Cluster 가 나왔지만, Falling back to default configuration 으로 기본 설정을 써서 갱신이 진행됐다. 새 인증서를 쓰려면 kube-apiserver, kube-controller-manager, kube-scheduler, etcd 를 재시작해야 한다. 이들은 Static Pod 로 돌기 때문에 containerd 와 kubelet 을 재시작해 다시 띄웠다. 마지막으로 홈 디렉터리의 kubeconfig 를 새 admin.conf 로 바꾸고 kubectl get nodes 같은 명령이 동작하는지 확인했다."},"trigger":{"en":"The leaf certificates that kubeadm had generated for the cluster, valid for one year by default, expired at 2026-02-20 16:16 UTC.","ko":"kubeadm 이 만든 하위 인증서의 기본 유효기간 1년이 2026-02-20 16:16 UTC 에 끝났다."},"severity":"unknown","symptoms":["control-plane-unresponsive","crash-loop"],"timeline":{"detected_after":null,"resolved_after":null},"date_kind":"incident","detection":{"en":"The author could not connect to the cluster from outside and went to the control plane node to read the kubelet logs.","ko":"외부에서 클러스터에 접속이 안 되는 것으로 알아챘다. 컨트롤 플레인 노드에서 kubelet 로그, containerd 로그, kube-apiserver 컨테이너 로그를 차례로 보다가 kube-apiserver 로그의 x509 만료 오류에서 원인을 확인했다."},"components":["kube-apiserver","etcd","kubelet","calico"],"root_cause":"certificate-expiry","remediation":[{"text":{"en":"renewed all expired certificates with sudo kubeadm certs renew all","ko":"sudo kubeadm certs renew all 로 만료된 인증서를 한 번에 갱신했다."}},{"text":{"en":"restarted containerd and the kubelet so the static control plane pods restarted with the new certificates","ko":"containerd 와 kubelet 을 재시작해 Static Pod 로 도는 컨트롤 플레인 컴포넌트를 새 인증서로 다시 띄웠다."}},{"text":{"en":"copied the renewed /etc/kubernetes/admin.conf to $HOME/.kube/config","ko":"/etc/kubernetes/admin.conf 를 $HOME/.kube/config 로 복사하고 소유자를 바꿔 kubeconfig 를 새 인증서로 바꿨다."}}],"action_items":[],"contributing":["default-config-unchanged"],"recurrence_of":null,"symptoms_text":{"en":"The cluster could not be reached from outside. On the control plane node, sudo systemctl status kubelet showed TLS handshake errors such as \"http: TLS handshake error from 101.198.0.183:19517: tls: client requested unsupported application protocols\" and \"http: TLS handshake error from 101.198.0.172:36328: tls: client offered only unsupported versions: [302 301]\".\n\nThe kubelet kept failing to register its node, \"Unable to register node with API server\" err=\"Post \"https://x.x.x.x:6443/api/v1/nodes\": dial tcp x.x.x.x:6443: ...\", and to renew its node lease, \"Failed to ensure lease exists, will retry\". containerd logged that the Calico CNI plugin could not tear down pod sandboxes because it could not reach the API server: \"plugin type=\"calico\" failed (delete): error getting ClusterInformation: Get \"https://10.96.0.1:443/apis/crd.projectcalico.org/v1/clusterinformations/default\": dial tcp 10.96.0.1:443: connect: no route to host\".\n\nsudo crictl ps -a showed kube-apiserver containers in state Exited with attempt counts that kept rising (7, 8 and 41): the API server died right after it started. The log of one of these containers showed only \"oidc-issuer-url and oidc-client-id must be specified together when any oidc-* flags are set\", which did not point to certificates. A more recent container's log held the real error, from kube-apiserver's connection to etcd at 127.0.0.1:2379: \"tls: failed to verify certificate: x509: certificate has expired or is not yet valid: current time 2026-02-22T06:12:12Z is after 2026-02-20T16:16:45Z\".\n\nsudo kubeadm certs check-expiration listed admin.conf, apiserver, apiserver-etcd-client, apiserver-kubelet-client, controller-manager.conf, etcd-healthcheck-client, etcd-peer, etcd-server, front-proxy-client, scheduler.conf and super-admin.conf as expiring Feb 20, 2026 16:16 UTC, with residual time <invalid>. The CA certificates ca, etcd-ca and front-proxy-ca were valid until Feb 18, 2035.","ko":"외부에서 쿠버네티스 클러스터에 접속할 수 없었다. 작성자는 컨트롤 플레인 노드로 들어가 sudo systemctl status kubelet 으로 kubelet 로그를 확인했다.\n\nkubelet 로그에는 http: TLS handshake error from 101.198.0.183:19517: tls: client requested unsupported application protocols 같은 TLS 핸드셰이크 오류가 찍혀 있었다. 이어서 \"Unable to register node with API server\" 오류로 노드 등록이 계속 실패했고, \"Failed to ensure lease exists, will retry\" 오류로 kube-node-lease 갱신도 실패했다. kube-node-lease 는 kubelet 이 주기적으로 하트비트를 보내 노드가 살아 있음을 알리는 리소스다.\n\ncontainerd 로그를 보면 Calico 도 API 서버에 닿지 못하고 있었다. StopPodSandbox 가 plugin type=\"calico\" failed (delete): error getting ClusterInformation: Get \"https://10.96.0.1:443/apis/crd.projectcalico.org/v1/clusterinformations/default\": dial tcp 10.96.0.1:443: connect: no route to host 로 실패했다. 10.96.0.1 은 kubernetes 서비스의 ClusterIP 다. API 서버가 동작하지 않아 네트워크 플러그인까지 줄줄이 실패하고 있었다.\n\nsudo crictl ps -a | grep kube-apiserver 로 보니 kube-apiserver 컨테이너는 Exited 상태였고 ATTEMPT 숫자가 계속 늘고 있었다. 시작하자마자 죽고 있었다. 처음 본 컨테이너 로그에는 oidc-issuer-url and oidc-client-id must be specified together when any oidc-* flags are set 만 있어서 인증서 문제인지 바로 알기 어려웠다. 더 최근 컨테이너의 로그에서 etcd 127.0.0.1:2379 에 접속할 때 tls: failed to verify certificate: x509: certificate has expired or is not yet valid: current time 2026-02-22T06:12:12Z is after 2026-02-20T16:16:45Z 오류가 나왔다."},"updated_at":"2026-11-09T11:55:57.098326Z","url":"https://benchlet.com/incidentlib/app/cases/IL-0183","org_slug":"ckck803"}