Troubleshooting Expired Certificates
kubeadm certificates last one year and renewing them is one command. The parts that catch people are that an expired client certificate reports Unauthorized rather than anything about dates, and that renewal does not touch the kubeconfig you copied to your home directory.
Troubleshooting Guide 96 of 103 Advanced
- Kubernetes1.36.4
- kubeadm1.36.4
- OpenSSL3.5.5
- Cluster1 node
- TimeAbout 34 min
- Reviewed23 August 2026
Written against the versions above. A kubeadm cluster. The expired certificate here is genuinely expired, signed with an explicit past validity window using OpenSSL 3, not simulated by changing the clock.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| CKA4001 | 192.168.0.191 | Ubuntu 26.04 LTS | Control Plane Node, Worker Node | 2 Core | 4 GB | 50 GB |
Before you start
- A kubeadm-built cluster and root access on a control plane node.
- The Control plane components guide, since the certificates map onto them.
- The Static Pods guide, because that is how the control plane restarts.
-
What kubeadm manages, and for how long
kubeadm certs check-expirationis the inventory, and the first thing to run on any cluster you have inherited.Everything it lists is signed by one of two CAs and lasts one year from the day the cluster was built. That is the fact behind this whole guide: a kubeadm cluster left alone for twelve months stops answering.
/etc/kubernetes/pki/holds the certificates and keys themselves. The four.conffiles beside them are kubeconfigs with a client certificate embedded in each - which is why they appear in the expiry list too, and why renewing is not only about thepkidirectory.bash Example session sudo -n kubeadm certs check-expiration 2>&1 | sed -n '/CERTIFICATE /,/^$/p' | head -12CERTIFICATE EXPIRES RESIDUAL TIME CERTIFICATE AUTHORITY EXTERNALLY MANAGEDadmin.conf Aug 21, 2027 13:00 UTC 363d ca noapiserver Aug 21, 2027 13:00 UTC 363d ca noapiserver-etcd-client Aug 21, 2027 13:00 UTC 363d etcd-ca noapiserver-kubelet-client Aug 21, 2027 13:00 UTC 363d ca nocontroller-manager.conf Aug 21, 2027 13:00 UTC 363d ca noetcd-healthcheck-client Aug 21, 2027 13:00 UTC 363d etcd-ca noetcd-peer Aug 21, 2027 13:00 UTC 363d etcd-ca noetcd-server Aug 21, 2027 13:00 UTC 363d etcd-ca nofront-proxy-client Aug 21, 2027 13:00 UTC 363d front-proxy-ca noscheduler.conf Aug 21, 2027 13:00 UTC 363d ca nosuper-admin.conf Aug 21, 2027 13:00 UTC 363d ca nosudo -n ls /etc/kubernetes/pki/ | tr '\n' ' '; echoapiserver-etcd-client.crt apiserver-etcd-client.key apiserver-kubelet-client.crt apiserver-kubelet-client.key apiserver.crt apiserver.key ca.crt ca.key ca.srl etcd front-proxy-ca.crt front-proxy-ca.key front-proxy-client.crt front-proxy-client.key sa.key sa.pubsudo -n ls /etc/kubernetes/*.conf | tr '\n' ' '; echo/etc/kubernetes/admin.conf /etc/kubernetes/controller-manager.conf /etc/kubernetes/kubelet.conf /etc/kubernetes/scheduler.conf /etc/kubernetes/super-admin.confExpected resultAn expiry table, the pki directory listing, and four kubeconfig files.
Success conditionYou know what expires on this cluster and where it lives.
-
One year against ten, read from the certificates
Do not take the table's word for it - read the certificates.
The apiserver certificate is good for a year. The CA is good for ten, and that difference is the one to remember:
kubeadm certs renewre-signs leaf certificates with a CA that is not itself being renewed. Rotating the CA is a different and much harder operation.The
subjectAltNamelist is worth a look while you are here. It is why the apiserver certificate is valid for the service IP, the node name andkubernetes.defaultall at once - and why adding a load balancer in front of a cluster usually means re-issuing it with a new name.bash Example session sudo -n openssl x509 -in /etc/kubernetes/pki/apiserver.crt -noout -subject -datessubject=CN=kube-apiservernotBefore=Aug 21 12:55:16 2026 GMTnotAfter=Aug 21 13:00:16 2027 GMTsudo -n openssl x509 -in /etc/kubernetes/pki/ca.crt -noout -subject -datessubject=CN=kubernetesnotBefore=Aug 20 19:34:18 2026 GMTnotAfter=Aug 17 19:39:18 2036 GMTsudo -n openssl x509 -in /etc/kubernetes/pki/apiserver.crt -noout -ext subjectAltName | tail -2X509v3 Subject Alternative Name: DNS:cka4001, DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, IP Address:10.96.0.1, IP Address:192.168.0.191Expected resultA one-year apiserver certificate, a ten-year CA, and the SAN list.
Success conditionYou can tell which certificate renewal will and will not replace.
-
The symptom: Unauthorized, saying nothing about dates
Rather than wait a year, mint a certificate that has already expired. OpenSSL 3.5 accepts explicit validity dates, so the cluster's own CA signs a client certificate that was valid for January 2024:
openssl x509 -req -in user.csr -CA ca.crt -CAkey ca.key -extfile ext.cnf \ -not_before 20240101000000Z -not_after 20240201000000Z -out old.crtopenssl x509 -checkend 0confirms it:Certificate will expire. The identity isCN=kubernetes-admin, O=system:masters- a full cluster administrator, so nothing that follows is a permissions problem.Point a kubeconfig at it and this is what you get:
error: You must be logged in to the server (Unauthorized)That is the whole difficulty of this failure. The message says nothing about certificates, nothing about dates, and nothing about expiry. It is identical to what a wrong CA, a missing certificate or a revoked identity would produce.
kubectl versionis slightly more forthcoming - it prints the client version, reaches the server, and reportsthe server has asked for the client to provide credentials.Then sign the same CSR again with normal dates. Same key, same subject, same CA - and
kubectl get nodesanswers. Nothing changed but the validity window, which is the proof that the dates were the problem all along.So the diagnostic habit worth building: when you meet
Unauthorized, do not reason about RBAC. Read the certificate's dates first.bash Example session rm -rf /tmp/exp && mkdir -p /tmp/exp && cd /tmp/exp && openssl genrsa -out user.key 2048 2>/dev/null && openssl req -new -key user.key -out user.csr -subj "/CN=kubernetes-admin/O=system:masters" && echo "extendedKeyUsage=clientAuth" > ext.cnf && lsext.cnfuser.csruser.keycd /tmp/exp && sudo -n openssl x509 -req -in user.csr -CA /etc/kubernetes/pki/ca.crt -CAkey /etc/kubernetes/pki/ca.key -CAcreateserial -extfile ext.cnf -not_before 20240101000000Z -not_after 20240201000000Z -out old.crt 2>&1 | tail -1subject=CN=kubernetes-admin, O=system:masterscd /tmp/exp && openssl x509 -in old.crt -noout -subject -issuer -datessubject=CN=kubernetes-admin, O=system:mastersissuer=CN=kubernetesnotBefore=Jan 1 00:00:00 2024 GMTnotAfter=Feb 1 00:00:00 2024 GMTcd /tmp/exp && openssl x509 -in old.crt -noout -checkend 0 || trueCertificate will expirekubectl --kubeconfig=/tmp/exp/stale.conf get nodes 2>&1 | head -3error: You must be logged in to the server (Unauthorized)kubectl --kubeconfig=/tmp/exp/stale.conf version 2>&1 | head -3Client Version: v1.36.4Kustomize Version: v5.8.1error: You must be logged in to the server (the server has asked for the client to provide credentials)cd /tmp/exp && sudo -n openssl x509 -req -in user.csr -CA /etc/kubernetes/pki/ca.crt -CAkey /etc/kubernetes/pki/ca.key -CAcreateserial -extfile ext.cnf -days 365 -out new.crt 2>&1 | tail -1subject=CN=kubernetes-admin, O=system:masterscd /tmp/exp && sudo -n chown $(id -un) new.crt && openssl x509 -in new.crt -noout -subject -datessubject=CN=kubernetes-admin, O=system:mastersnotBefore=Aug 23 01:43:05 2026 GMTnotAfter=Aug 23 01:43:05 2027 GMTcd /tmp/exp && cp stale.conf fresh.conf && kubectl config set-credentials u --client-certificate=/tmp/exp/new.crt --client-key=/tmp/exp/user.key --embed-certs=true --kubeconfig=/tmp/exp/fresh.conf >/dev/null && kubectl --kubeconfig=/tmp/exp/fresh.conf get nodes --no-headers 2>&1 | head -2cka4001 Ready control-plane 2d6h v1.36.4Expected resultA certificate dated 2024,
Certificate will expire,Unauthorizedfrom kubectl, and the same identity working once re-signed.Success conditionYou have produced the exact error an expired certificate gives, and cleared it.
-
Renew everything with one command
kubeadm certs renew allre-issues every certificate the cluster signs for itself, including the ones embedded inadmin.conf,controller-manager.confandscheduler.conf.The apiserver certificate's
notAftermoves out a year on disk, andcheck-expirationreports a full 364 days across the board.It works even when the certificates have already expired, which is the important property when you are recovering rather than maintaining.
kubeadmsigns with the CA key directly - it does not authenticate to the API server to do this - so it is unaffected by the very expiry you are fixing.bash Example session sudo -n openssl x509 -in /etc/kubernetes/pki/apiserver.crt -noout -enddatenotAfter=Aug 21 13:00:16 2027 GMTsudo -n kubeadm certs renew all 2>&1 | tail -12certificate for serving the Kubernetes API renewedcertificate the apiserver uses to access etcd renewedcertificate for the API server to connect to kubelet renewedcertificate embedded in the kubeconfig file for the controller manager to use renewedcertificate for liveness probes to healthcheck etcd renewedcertificate for etcd nodes to communicate with each other renewedcertificate for serving etcd renewedcertificate for the front proxy client renewedcertificate embedded in the kubeconfig file for the scheduler manager to use renewedcertificate embedded in the kubeconfig file for the super-admin renewed Done renewing certificates. You must restart the kube-apiserver, kube-controller-manager, kube-scheduler and etcd, so that they can use the new certificates.sudo -n kubeadm certs check-expiration 2>&1 | sed -n '/CERTIFICATE /,/^$/p' | head -6CERTIFICATE EXPIRES RESIDUAL TIME CERTIFICATE AUTHORITY EXTERNALLY MANAGEDadmin.conf Aug 23, 2027 01:43 UTC 364d ca noapiserver Aug 23, 2027 01:43 UTC 364d ca noapiserver-etcd-client Aug 23, 2027 01:43 UTC 364d etcd-ca noapiserver-kubelet-client Aug 23, 2027 01:43 UTC 364d ca nocontroller-manager.conf Aug 23, 2027 01:43 UTC 364d ca noExpected resultA new
notAfteron disk and 364 days in the expiry table.Success conditionEvery certificate on disk is freshly signed.
-
What the restart is actually for
kubeadm ends with
You must restart the kube-apiserver, kube-controller-manager, kube-scheduler and etcd. That is a safe instruction and an imprecise one, and the difference is worth measuring.Renew only the apiserver's serving certificate and watch the container:
CONTAINER CREATED STATE NAME ATTEMPT 084bcea0356db About a minute ago Running kube-apiserver 0 served before notAfter=Aug 23 01:44:50 2027 GMT file on disk notAfter=Aug 23 01:45:26 2027 GMT served after notAfter=Aug 23 01:45:26 2027 GMTSame container ID. Same
ATTEMPT 0. New certificate being served. The apiserver watches its serving certificate and reloads it, so no restart was involved.The restart is for everything that is not the serving certificate: the client certificates the apiserver presents to etcd and to kubelets, and the credentials baked into
controller-manager.confandscheduler.conf. Those are read once at startup. Skip the restart and those components keep using the old ones until they restart for some other reason - which on a quiet cluster may be never, and which is how a cluster ends up half-renewed.The control plane runs as static pods (guide 14), so the restart is a file move: take the manifest out of
/etc/kubernetes/manifests/, wait, put it back. Expect kubectl to fail while it is gone - that is the API server being down, and it is why this belongs in a maintenance window on a single-control-plane cluster. The API came back 15 seconds after the manifest was restored.bash Example session sudo -n crictl ps --name kube-apiserver -o 'table' 2>/dev/null | tail -2CONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID POD NAMESPACE084bcea0356db b0f70fa6ec47e About a minute ago Running kube-apiserver 0 2f624d30a97e5 kube-apiserver-cka4001 kube-systemecho | openssl s_client -connect 127.0.0.1:6443 2>/dev/null | openssl x509 -noout -enddatenotAfter=Aug 23 01:44:50 2027 GMTsudo -n kubeadm certs renew apiserver 2>&1 | tail -2 certificate for serving the Kubernetes API renewedsudo -n openssl x509 -in /etc/kubernetes/pki/apiserver.crt -noout -enddatenotAfter=Aug 23 01:45:26 2027 GMTsudo -n mv /etc/kubernetes/manifests/kube-apiserver.yaml /tmp/exp/ && sleep 20 && sudo -n mv /tmp/exp/kube-apiserver.yaml /etc/kubernetes/manifests/ && echo "manifest restored"manifest restoredfor i in $(seq 1 40); do kubectl get --raw='/readyz' >/dev/null 2>&1 && { echo "API server back after ${i}s"; break; }; sleep 1; doneAPI server back after 15skubectl get nodes --no-headerscka4001 Ready control-plane 2d6h v1.36.4Expected resultAn unchanged container serving a changed certificate, then a clean restart with the API back in about fifteen seconds.
Success conditionYou can say which certificates reload themselves and which need the restart.
-
The kubeconfig in your home directory is a copy, and renewal misses it
This is the part that turns a fixed cluster back into a broken one.
check-expirationreportsadmin.confat 364 days, and reading the certificate out of/etc/kubernetes/admin.confconfirms it. Now read the one yourkubectlis actually using:/etc/kubernetes/admin.conf notAfter=Aug 23 01:43:30 2027 GMT ~/.kube/config notAfter=Aug 20 19:39:18 2027 GMTTwo different certificates.
~/.kube/configwas copied fromadmin.confwhen the cluster was built, andkubeadm certs renewrewrote the original, not your copy.ls -lshows the copy's own date, unchanged by the renewal.And
kubectl get nodesstill works, which is exactly what makes this dangerous. The stale certificate has not expired yet - it simply expires on the original schedule. You renewed the cluster, verified it, and left your own credentials due to fail on the date you thought you had moved.The fix is a copy:
sudo cp /etc/kubernetes/admin.conf ~/.kube/config sudo chown $(id -u):$(id -g) ~/.kube/configDo it every time you renew, and check any other machine holding a copy - a laptop, a CI runner, a colleague. Each is a separate copy with its own expiry, and
check-expirationon the control plane knows about none of them.bash Example session sudo -n kubeadm certs check-expiration 2>&1 | grep -E 'admin.conf|super-admin.conf|controller-manager.conf|scheduler.conf'admin.conf Aug 23, 2027 01:43 UTC 364d ca nocontroller-manager.conf Aug 23, 2027 01:43 UTC 364d ca noscheduler.conf Aug 23, 2027 01:43 UTC 364d ca nosuper-admin.conf Aug 23, 2027 01:43 UTC 364d ca nosudo -n grep client-certificate-data /etc/kubernetes/admin.conf | awk '{print $2}' | base64 -d | openssl x509 -noout -enddatenotAfter=Aug 23 01:43:30 2027 GMTkubectl config view --raw -o jsonpath='{.users[0].user.client-certificate-data}' | base64 -d | openssl x509 -noout -enddatenotAfter=Aug 20 19:39:18 2027 GMTls -l ~/.kube/config | awk '{print $5, $6, $7, $8, $9}'5660 Aug 22 15:11 /home/candidate/.kube/configkubectl get nodes --no-headers 2>&1 | head -2cka4001 Ready control-plane 2d6h v1.36.4Expected resultThe renewed date on admin.conf, an older one in
~/.kube/config, and kubectl still working.Success conditionYou know to re-copy the kubeconfig, and why nothing warned you.
Troubleshooting
kubectlreturnsUnauthorizedorthe server has asked for the client to provide credentials.Why: The client certificate was rejected during the TLS handshake, most often because it expired.
Fix:Check the dates before checking RBAC:
kubectl config view --raw -o jsonpath='{.users[0].user.client-certificate-data}' | base64 -d | openssl x509 -noout -dates. The message never mentions expiry because authentication never happened.Forbiddenis the opposite situation: authentication worked and permissions did not.Certificates were renewed and the cluster is still broken.
Why: The components read certificates at startup and have not restarted.
Fix:kubeadm says so in its own output. Restart each control plane component by moving its manifest out of
/etc/kubernetes/manifestsand back, or restart the kubelet. Verify from the wire, not the disk:echo | openssl s_client -connect 127.0.0.1:6443 | openssl x509 -noout -dates.Certificates were renewed and
kubectlstill fails for your user.Why:
~/.kube/configis a copy ofadmin.confmade at install time. Renewal does not touch it.Fix:
sudo cp /etc/kubernetes/admin.conf ~/.kube/config && sudo chown $(id -u):$(id -g) ~/.kube/config. Then find every other copy: CI secrets, colleagues' machines, credential stores. Each holds its own certificate with its own expiry and none of them renews.kubectlcannot reach the cluster and you cannot run kubeadm through it.Why: Nothing.
kubeadm certs renewdoes not need the API server.Fix:Renewal is a local operation using the CA keys on disk, so it works on a cluster that is refusing every connection. Run it on the control plane node, restart the components, then check
kubectl. This is the normal recovery path for an expired cluster.x509: certificate signed by unknown authority.Why: The client does not trust the server's certificate. This is the reverse of an expiry problem.
Fix:The
certificate-authority-datain your kubeconfig no longer matches the cluster's CA, which happens after a rebuild or when pointing at a different cluster. Take a fresh kubeconfig from the control plane node. Renewing certificates does not cause this, because renewal keeps the same CA.x509: certificate has expired or is not yet validon a certificate that looks fine.Why: Read the second half of that message. A wrong system clock makes valid certificates appear not-yet-valid.
Fix:Check the time on both machines,
timedatectl, before touching any certificate. Clock skew produces certificate errors that look like expiry, and it is quick to rule out.A new DNS name or load balancer in front of the API server fails certificate validation.
Why: The name is not in the API server certificate's subjectAltName.
Fix:
openssl x509 -in /etc/kubernetes/pki/apiserver.crt -noout -ext subjectAltName. Add the name toapiServerCertSANsin the kubeadm config, thenkubeadm certs renew apiserverand restart it. A plain renew keeps the existing SANs, so the config change has to come first.A node that was switched off for a long time will not rejoin.
Why: The kubelet rotates its own client certificate, but only while the current one is still valid. An expired one cannot renew itself.
Fix:Generate a new bootstrap token on a control plane node and rejoin the node with
kubeadm join. Deleting/var/lib/kubelet/pki/kubelet-client-current.pemand restarting the kubelet also works when a valid bootstrap credential is present.EXTERNALLY MANAGED: yesin the expiry table andkubeadm certs renewwill not renew.Why: The CA key is not on this machine, so kubeadm cannot sign.
Fix:The certificates come from an external PKI and that PKI must reissue them.
kubeadm certs check-expirationstill reports the dates, so it remains useful for monitoring even where it cannot renew.