Containers

Fix Expired Kubernetes Certificates on a kubeadm Cluster

A kubeadm cluster doesn’t fade out when its certificates lapse. It stops on the anniversary of kubeadm init, because every client certificate kubeadm signs is valid for exactly one year. kubectl fails with x509: certificate has expired or is not yet valid, the controller-manager loses its lease, and a worker whose kubelet client certificate also ran out drops to NotReady. A kubernetes certificate expired outage is at least dated and predictable, so once you know which file each error points at, the recovery is short.

Original content from computingforgeeks.com - post 181205

We start from the error text, map each line to the certificate behind it, then renew the control plane with kubeadm certs renew all, restart the static pods and refresh admin.conf. After that we rebuild a worker’s kubelet.conf end to end, the part the official troubleshooting page compresses into seven terse steps. We built a two-node Kubernetes v1.37.2 cluster on Ubuntu 24.04 in November 2026 and moved both clocks 370 days forward, so every error line and output below comes from that cluster breaking and recovering.

How kubeadm certificates expire

kubeadm creates three certificate authorities at init time (ca, etcd-ca and front-proxy-ca) and signs everything else from them. The CAs get ten years, while every leaf certificate gets one year, and the failure lands on a single day because all leaf certificates were issued in the same second and so expire in the same second. Clusters that run kubeadm upgrade at least once a year never notice, because the upgrade renews every control-plane certificate as a side effect.

Kubelets are the exception to that clock. kubeadm sets rotateCertificates: true in the KubeletConfiguration, so each kubelet requests a fresh client certificate from the controller-manager’s signer before the old one runs out and stores it under /var/lib/kubelet/pki/. That rotation only works while the kubelet is running and can still authenticate. In our lab the control-plane kubelet on cp01 rotated as expected, while worker01, which we had stopped to play a powered-off node, kept its original certificate and came back with a dead one. The diagram shows which certificate each connection presents:

kubeadm certificate architecture diagram: kubectl, controller-manager, scheduler and the worker kubelet connect to kube-apiserver on 6443 with client certificates, kube-apiserver reaches etcd with apiserver-etcd-client and the kubelet with apiserver-kubelet-client

Each arrow is a TLS connection, and each one fails with a slightly different message when the certificate on either end has expired. The table below lists the files kubeadm certs renew all rewrites, plus the kubelet certificate it does not touch.

CertificateFileWho presents itWhat fails when it expires
apiserver/etc/kubernetes/pki/apiserver.crtkube-apiserver, serving on 6443Every client that verifies the server, kubectl first
admin.conf/etc/kubernetes/admin.conf (embedded)kubectl as kubernetes-adminkubectl gets 401
controller-manager.conf, scheduler.conf/etc/kubernetes/*.conf (embedded)Controller-manager, schedulerNo reconciling, no scheduling, CSR signing stops
apiserver-etcd-client/etc/kubernetes/pki/apiserver-etcd-client.crtkube-apiserver talking to etcdetcd refuses new API server connections
etcd-server, etcd-peer, etcd-healthcheck-client/etc/kubernetes/pki/etcd/etcdAPI server cannot verify etcd, HA members cannot peer
apiserver-kubelet-client/etc/kubernetes/pki/apiserver-kubelet-client.crtkube-apiserver calling kubelets on 10250kubectl logs and exec fail
front-proxy-client/etc/kubernetes/pki/front-proxy-client.crtAggregation layerAggregated APIs such as metrics.k8s.io
kubelet client/var/lib/kubelet/pki/kubelet-client-current.pemkubelet on every nodeNode goes NotReady; not renewed by kubeadm

On worker nodes, the last row matters most. kubeadm certs check-expiration doesn’t list kubelet.conf at all, because the docs treat it as self-rotating. A check run on the control plane can therefore report every certificate healthy while a worker sits NotReady with an expired client certificate, which is why we check the kubelet separately below.

What each x509 error is telling you

Every expiry message on this cluster comes from the same line in the Go crypto/x509 package, which prints x509: certificate has expired or is not yet valid: followed by current time ... is after ... with two RFC 3339 timestamps. The wording is identical whichever certificate failed, so the useful information is where the line appears and which side of the connection logged it. When kubectl is dead, you read the API server through the container runtime instead. kubeadm doesn’t install crictl, so add the cri-tools package from the same pkgs.k8s.io repository first:

sudo apt-get install -y cri-tools

Then filter the log of the API server container for x509 lines, which shows who is still knocking with an old certificate:

sudo crictl logs $(sudo crictl ps -a --name kube-apiserver -q | head -1) 2>&1 | grep -i x509 | tail -5

On our expired cp01 the API server was rejecting the clients that still presented year-old certificates (lines shortened at the certificate serial):

E1106 01:51:35.873297       1 authentication.go:76] "Unable to authenticate the request" err="[x509: certificate has expired or is not yet valid: current time 2027-11-06T01:51:35Z is after 2027-11-01T01:43:25Z, verifying certificate SN=8728276683957971603, ...
E1106 01:51:36.113533       1 authentication.go:76] "Unable to authenticate the request" err="[x509: certificate has expired or is not yet valid: current time 2027-11-06T01:51:36Z is after 2027-11-01T01:43:25Z, verifying certificate SN=115837504332588788, ...

In the terminal on cp01 these lines are several hundred characters long (shortened again here), which is why the tail -5 matters on a busy API server:

kube-apiserver log lines Unable to authenticate the request with x509 certificate has expired, read through crictl logs

On a worker, the kubelet journal is the place to look, and filtering it for certificate words keeps the noise down:

sudo journalctl -u kubelet --since "10 minutes ago" --no-pager | grep -iE 'x509|bootstrap|expired|Unauthorized' | tail -20

Four messages came out of our lab, and each H3 below is the string you’ll find in one of those places.

Unable to connect to the server: tls: failed to verify certificate: x509: certificate has expired or is not yet valid

kubectl printed this, so the expired certificate is the one the API server presented: apiserver.crt. kubectl never got far enough to send its own credentials. Here is kubectl get nodes on cp01, the first of five retries and then the final line:

E1106 01:52:26.466410   11109 memcache.go:381] "Couldn't get current server API group list" err="Get \"https://10.0.1.10:6443/api?timeout=32s\": tls: failed to verify certificate: x509: certificate has expired or is not yet valid: current time 2027-11-06T01:52:26Z is after 2027-11-01T01:43:25Z"
Unable to connect to the server: tls: failed to verify certificate: x509: certificate has expired or is not yet valid: current time 2027-11-06T01:52:26Z is after 2027-11-01T01:43:25Z

This is how it looked on the cp01 console, with the retry lines filtered out:

kubectl get nodes on a kubeadm v1.37.2 control plane failing with tls: failed to verify certificate: x509: certificate has expired or is not yet valid

The controller-manager logged the same string while trying to hold its leader lease, which is why nothing reconciles during the outage. Its log showed "Error retrieving lease lock" err="Get \"https://10.0.1.10:6443/apis/coordination.k8s.io/v1/namespaces/kube-system/leases/kube-controller-manager?timeout=5s\": tls: failed to verify certificate: x509: certificate has expired or is not yet valid: ... every two to three seconds. etcd, by contrast, logged no x509 errors at all: it was still running with the certificates it loaded a year earlier, so its log is not where the outage shows.

error: You must be logged in to the server (the server has asked for the client to provide credentials)

Here the TLS handshake succeeded, which means the server certificate is fine, but the API server rejected the client certificate in your kubeconfig with a 401. Some kubectl versions and commands print (Unauthorized) instead. We hit it twice in the lab, both times with the new apiserver.crt already on disk and the stale ~/.kube/config still in place:

E1106 01:53:14.661826   11417 memcache.go:381] "Couldn't get current server API group list" err="the server has asked for the client to provide credentials"
error: You must be logged in to the server (the server has asked for the client to provide credentials)

On a kubeadm cluster this is almost always a copy of admin.conf that went stale, and the fix is a fresh copy, covered further down.

part of the existing bootstrap client certificate in /etc/kubernetes/kubelet.conf is expired

The kubelet source file bootstrap.go logs this at startup when the client certificate it was about to use has passed its NotAfter date. kubeadm removes the bootstrap kubeconfig once a node has joined, so the kubelet’s fallback finds nothing and the process exits. These are the matching lines from worker01’s journal after we started its kubelet again (config-dump lines dropped; the next restart ten seconds later printed the same three):

Nov 06 01:52:44 worker01 kubelet[10318]: I1106 01:52:44.652061   10318 server.go:1036] "Client rotation is on, will bootstrap in background"
Nov 06 01:52:44 worker01 kubelet[10318]: E1106 01:52:44.653023   10318 bootstrap.go:267] "Unhandled Error" err="part of the existing bootstrap client certificate in /etc/kubernetes/kubelet.conf is expired: 2027-11-01 01:41:08 +0000 UTC" logger="UnhandledError"
Nov 06 01:52:44 worker01 kubelet[10318]: E1106 01:52:44.653060   10318 run.go:72] "command failed" err="failed to run Kubelet: unable to load bootstrap kubeconfig: stat /etc/kubernetes/bootstrap-kubelet.conf: no such file or directory"

On worker01 the three lines repeat with every restart attempt:

worker kubelet journal showing part of the existing bootstrap client certificate in /etc/kubernetes/kubelet.conf is expired and unable to load bootstrap kubeconfig

The message names kubelet.conf, but on our worker that file only pointed at /var/lib/kubelet/pki/kubelet-client-current.pem; the date at the end of the line is the NotAfter of that PEM. systemd restarts the kubelet every ten seconds and it fails the same way each time (Active: activating (auto-restart) (Result: exit-code) in systemctl status kubelet), so the node stays NotReady until you rebuild the file.

x509: certificate has expired or is not yet valid: current time … is before …

Read the last clause carefully. “is after” means a real expiry, while “is before” means the NotBefore date of the certificate sits in the future from the point of view of this machine, which is a clock problem and not a certificate problem. A node whose clock jumped backwards, or a freshly renewed control plane talking to a node that is minutes behind, produces exactly this. Check the clock before renewing anything:

timedatectl status

You want System clock synchronized: yes on every node. Ours read no with NTP service: inactive because we turned NTP off to move the clock; on a real cluster that combination is the thing to fix with chrony or systemd-timesyncd first, because renewing certificates on a host with the wrong time just mints a new set with the wrong validity window.

Which certificates expired on your cluster

kubeadm reads the expiry dates straight from the files on disk, so this check works even when the API server is down. It first tries the kubeadm-config ConfigMap and, on a broken cluster, prints a fallback warning before carrying on. Run it on a control-plane node:

sudo kubeadm certs check-expiration

Every leaf certificate on cp01 showed <invalid> in the RESIDUAL TIME column, including super-admin.conf, which exists only on the node where kubeadm init ran. The CAs still had eight years left:

[check-expiration] Reading configuration from the "kubeadm-config" ConfigMap in namespace "kube-system"...
[check-expiration] Use 'kubeadm init phase upload-config kubeadm --config your-config-file' to re-upload it.
[check-expiration] Error reading configuration from the Cluster. Falling back to default configuration

CERTIFICATE                EXPIRES                  RESIDUAL TIME   CERTIFICATE AUTHORITY   EXTERNALLY MANAGED
admin.conf                 Nov 01, 2027 01:43 UTC   <invalid>       ca                      no
apiserver                  Nov 01, 2027 01:43 UTC   <invalid>       ca                      no
apiserver-etcd-client      Nov 01, 2027 01:43 UTC   <invalid>       etcd-ca                 no
apiserver-kubelet-client   Nov 01, 2027 01:43 UTC   <invalid>       ca                      no
controller-manager.conf    Nov 01, 2027 01:43 UTC   <invalid>       ca                      no
etcd-healthcheck-client    Nov 01, 2027 01:43 UTC   <invalid>       etcd-ca                 no
etcd-peer                  Nov 01, 2027 01:43 UTC   <invalid>       etcd-ca                 no
etcd-server                Nov 01, 2027 01:43 UTC   <invalid>       etcd-ca                 no
front-proxy-client         Nov 01, 2027 01:43 UTC   <invalid>       front-proxy-ca          no
scheduler.conf             Nov 01, 2027 01:43 UTC   <invalid>       ca                      no
super-admin.conf           Nov 01, 2027 01:43 UTC   <invalid>       ca                      no

CERTIFICATE AUTHORITY   EXPIRES                  RESIDUAL TIME   EXTERNALLY MANAGED
ca                      Oct 29, 2036 01:43 UTC   8y              no
etcd-ca                 Oct 29, 2036 01:43 UTC   8y              no
front-proxy-ca          Oct 29, 2036 01:43 UTC   8y              no

On the cp01 terminal the <invalid> column is the one to scan, since every row in it is a certificate kubeadm will renew:

kubeadm certs check-expiration listing every leaf certificate with residual time invalid while the CAs show 8y

The “Falling back to default configuration” line is expected on a dead cluster and harmless here. If the EXTERNALLY MANAGED column says yes for anything, kubeadm can’t renew that certificate because it does not hold the signing key. Because the kubelet certificate is missing from that output, check it directly on each node with openssl:

sudo openssl x509 -noout -subject -enddate -in /var/lib/kubelet/pki/kubelet-client-current.pem

On worker01 the subject was right and the date had passed five days earlier:

subject=O = system:nodes, CN = system:node:worker01
notAfter=Nov  1 01:41:08 2027 GMT

A notAfter in the past means that node needs the rebuild further down. If the file is missing, or /etc/kubernetes/kubelet.conf carries client-certificate-data instead of a path, the same rebuild applies. For the same check against any other PEM file or remote endpoint, our openssl expiration guide has the variants.

How kubeadm certs renew all fixes the control plane

The renewal re-signs every leaf certificate with the CA keys already sitting in /etc/kubernetes/pki, reusing the existing Common Name, Organization and SANs from the old files rather than reading the kubeadm-config ConfigMap. That’s what makes it safe on a cluster whose API is down, because nothing it signs depends on the cluster answering. Back up the whole directory first, since a renewal overwrites files in place:

sudo cp -a /etc/kubernetes /etc/kubernetes.bak-$(date +%F)

With the backup in place, renew everything kubeadm manages:

sudo kubeadm certs renew all

After the same fallback warning, kubeadm names each of the 11 certificates and ends with the instruction most people skip:

...
[renew] Error reading configuration from the Cluster. Falling back to default configuration

certificate embedded in the kubeconfig file for the admin to use and for kubeadm itself renewed
certificate for serving the Kubernetes API renewed
certificate the apiserver uses to access etcd renewed
certificate for the API server to connect to kubelet renewed
certificate embedded in the kubeconfig file for the controller manager to use renewed
certificate for liveness probes to healthcheck etcd renewed
certificate for etcd nodes to communicate with each other renewed
certificate for serving etcd renewed
certificate for the front proxy client renewed
certificate embedded in the kubeconfig file for the scheduler manager to use renewed
certificate 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.

On the cp01 terminal the same run ends on the line you have to act on:

kubeadm certs renew all output renewing 11 certificates and asking to restart kube-apiserver, controller-manager, scheduler and etcd

One thing surprised us. Seconds after the renewal, before any restart, kubectl stopped printing the x509 error and switched to the credentials error from earlier. kube-apiserver had already picked the new serving certificate up from disk, so the TLS handshake passed and only the stale client certificate in ~/.kube/config failed. That partial recovery is easy to misread as “fixed”. etcd, the controller-manager and the scheduler still hold their old certificates in memory, and the upstream docs say plainly that dynamic reload is not supported for all components, so the restart is still required.

Why the static pods need a restart

Control-plane components run as static pods, which the kubelet starts from YAML files in /etc/kubernetes/manifests/, not the API server. So kubectl delete pod can’t restart them. The documented method is to take the manifests away from the kubelet, wait for it to stop the pods, then give them back. Move them to a directory outside the manifests path:

sudo mkdir -p /etc/kubernetes/manifests-paused
sudo mv /etc/kubernetes/manifests/*.yaml /etc/kubernetes/manifests-paused/

The docs suggest waiting 20 seconds (the kubelet’s fileCheckFrequency), but kube-apiserver takes longer to drain. Check with crictl:

sudo crictl ps --name 'kube-apiserver|kube-controller-manager|kube-scheduler|etcd'

At 25 seconds our API server was still up while the other three had gone:

CONTAINER           IMAGE               CREATED             STATE               NAME                ATTEMPT             POD ID              POD                   NAMESPACE
be682b2421356       7a918dfe48696       12 months ago       Running             kube-apiserver      0                   4b2ee4896eb4e       kube-apiserver-cp01   kube-system

At 45 seconds only the header row printed. Wait for that empty list, then put the manifests back and the kubelet recreates each pod with the renewed certificates:

sudo mv /etc/kubernetes/manifests-paused/*.yaml /etc/kubernetes/manifests/
sudo rmdir /etc/kubernetes/manifests-paused

Run the same crictl ps about 40 seconds later and all four should be back with ATTEMPT 0 and a fresh CREATED time:

crictl ps showing kube-apiserver, kube-controller-manager, kube-scheduler and etcd running again 39 seconds after the manifests were restored

Moving all four at once is fine on a single control-plane node, since the cluster is already down. On an HA control plane, the section further down explains why you go one node at a time instead.

Why admin.conf has to be copied again

The renewal rewrote /etc/kubernetes/admin.conf, but kubectl reads ~/.kube/config, which on most kubeadm clusters is a copy made on install day. That copy still embeds the expired client certificate, so after the restart kubectl still printed You must be logged in to the server (the server has asked for the client to provide credentials). Replace it:

sudo cp /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

Now ask the cluster for its nodes:

kubectl get nodes -o wide

The control plane answered again, and the output separates the two problems cleanly. cp01 was Ready because its own kubelet had rotated, while worker01 stayed NotReady:

kubectl get nodes after copying admin.conf: control plane cp01 Ready, worker01 NotReady

A second kubeadm certs check-expiration now shows every leaf certificate at Nov 05, 2028 with 364d remaining. If you handed copies of admin.conf to CI jobs or colleagues, those copies are expired too and need the new file. kubeadm also renews super-admin.conf, the break-glass credential in the system:masters group. Keep that one on the node rather than in a personal kubeconfig.

How to rebuild an expired kubelet.conf on a worker

A worker with a dead client certificate cannot request a new one, because the request itself has to be authenticated. The way out is to sign a fresh client certificate for that node’s identity on a control-plane node, which holds ca.key, and hand it to the kubelet as a one-time credential. The upstream troubleshooting page lists this as seven steps without the commands, so the full sequence follows in order:

  1. Back up the worker’s kubelet.conf and PKI directory, and note the API server endpoint it used.
  2. Sign a new kubeconfig for system:node:<name> on a control-plane node.
  3. Install it on the worker and restart the kubelet so it writes kubelet-client-current.pem.
  4. Repoint kubelet.conf at that rotating file and restart again.

The node name must match exactly what kubectl get nodes shows, because the Node authorizer in the API server ties the certificate’s CN to that Node object. On the worker, back up and look at the old file before touching anything:

sudo mkdir -p /root/kubelet-backup
sudo cp -a /etc/kubernetes/kubelet.conf /var/lib/kubelet/pki /root/kubelet-backup/
sudo grep -E 'server:|client-certificate|client-key' /etc/kubernetes/kubelet.conf

worker01’s file was a normal kubeadm join result, pointing at the rotating PEM that had expired:

    server: https://10.0.1.10:6443
    client-certificate: /var/lib/kubelet/pki/kubelet-client-current.pem
    client-key: /var/lib/kubelet/pki/kubelet-client-current.pem

Note the server: URL (on an HA cluster it’s the load balancer address). If your file shows client-certificate-data instead, the node never rotated at all, and the same fix applies. Now switch to the control-plane node and export the two values the next commands reuse:

export NODE=worker01
export APISERVER=https://10.0.1.10:6443

Generate a kubeconfig whose client certificate carries the organization system:nodes and the CN system:node:worker01, which is the identity the kubelet authenticates as. The umask 077 keeps the file, which carries a private key, readable only by you from the moment it is written:

umask 077
sudo kubeadm kubeconfig user --org system:nodes --client-name "system:node:${NODE}" > "kubelet-${NODE}.conf"

Without a --config file, kubeadm fills the server and cluster name from its defaults, and the docs warn that you may need to adjust both by hand. Compare the generated server line with the one you noted on the worker:

grep -E 'server:|cluster:|name:' "kubelet-${NODE}.conf"

On our single control-plane cluster the defaults matched, so the file was usable as generated:

- cluster:
    server: https://10.0.1.10:6443
  name: kubernetes
    cluster: kubernetes
  name: system:node:worker01@kubernetes
- name: system:node:worker01

If the server URL differs, which is common when the cluster sits behind a load balancer in front of the API server, set it to the worker’s original value:

sed -i "s#server: .*#server: ${APISERVER}#" "kubelet-${NODE}.conf"

We copy the file over SSH and delete the local copy straight after, because it holds a private key for the node identity. Replace 10.0.1.11 with the worker’s address:

scp "kubelet-${NODE}.conf" 10.0.1.11:/tmp/kubelet.conf
rm -f "kubelet-${NODE}.conf"

Back on the worker, remove the old kubeconfig and the dead client certificates, then install the new file with root-only permissions. The sh -c wrapper matters because /var/lib/kubelet is mode 0750, so your own shell can’t expand the glob:

sudo sh -c 'rm -f /etc/kubernetes/kubelet.conf /var/lib/kubelet/pki/kubelet-client*'
sudo install -o root -g root -m 600 /tmp/kubelet.conf /etc/kubernetes/kubelet.conf
sudo rm -f /tmp/kubelet.conf
sudo systemctl restart kubelet

The kubelet treats the embedded certificate as a bootstrap credential: it authenticates with it, files a CSR for a proper rotating certificate, and writes the result to the PKI directory. Give it ten seconds and list the directory:

sudo ls -l /var/lib/kubelet/pki/

A new timestamped PEM and the kubelet-client-current.pem symlink appeared within those ten seconds, with no manual CSR approval:

total 12
-rw------- 1 root root 1110 Nov  6 01:55 kubelet-client-2027-11-06-01-55-37.pem
lrwxrwxrwx 1 root root   59 Nov  6 01:55 kubelet-client-current.pem -> /var/lib/kubelet/pki/kubelet-client-2027-11-06-01-55-37.pem
-rw-r--r-- 1 root root 2270 Nov  1  2026 kubelet.crt
-rw------- 1 root root 1675 Nov  1  2026 kubelet.key

Until that symlink exists, the next step has nothing to point at, so don’t rush it. If it never appears, check the kubelet journal for a new error before continuing.

The kubelet now rotates from that file, but kubelet.conf still embeds the certificate you installed, and that embedded copy never changes. On a restart after it expires, the kubelet would load the stale embedded certificate and fail again. Swap the two embedded lines for references to the rotating file, which is the edit that turns a one-year fix into a permanent one:

sudo sed -i \
  -e 's#client-certificate-data: .*#client-certificate: /var/lib/kubelet/pki/kubelet-client-current.pem#' \
  -e 's#client-key-data: .*#client-key: /var/lib/kubelet/pki/kubelet-client-current.pem#' \
  /etc/kubernetes/kubelet.conf
sudo systemctl restart kubelet

Confirm the edit landed, the kubelet stayed up and the certificate it now uses is the new one:

sudo grep -E 'client-certificate|client-key' /etc/kubernetes/kubelet.conf
systemctl is-active kubelet
sudo openssl x509 -noout -subject -enddate -in /var/lib/kubelet/pki/kubelet-client-current.pem

On worker01, with the PKI listing from the previous step on top, both client lines name the rotating file, the service is active, and the certificate runs for another year:

worker01 kubelet pki directory with a new kubelet-client-current.pem and kubelet.conf pointing at the rotating file, valid to November 2028

The NotAfter lands five minutes short of a full year after the file timestamp because the signer in the controller-manager backdates NotBefore to absorb clock skew. Finish on the control-plane node:

kubectl get nodes
kubectl get csr

worker01 was Ready within a minute of the kubelet.conf swap, and the CSR it filed during the restart (csr-7mclp) shows Approved,Issued:

kubectl get nodes showing both nodes Ready and kubectl get csr with the worker01 CSR Approved,Issued after the kubelet.conf rebuild

The list also tells the history of the cluster: csr-q8ffz is worker01’s original join, and csr-xfcq6 is the cp01 kubelet rotating 35 days before expiry, which is exactly what the worker missed while it was off. (After our clock jump the cp01 kubelet only noticed once restarted; a kubelet running through a real year rotates by itself.) Auto-approval works because kubeadm binds nodes to the role that lets the controller-manager approve their own client certificates. A CSR stuck in Pending usually means the controller-manager is still running with its old certificate, so go back and check the static pod restart. Repeat the whole sequence for each NotReady worker, changing NODE and the scp target each time.

What changes on an HA control plane

Certificates live on each control-plane node separately, and kubeadm certs renew all only touches the node it runs on. The kubeadm docs are explicit that you run it on every control-plane node. Run the backup, renewal and static pod restart on one node, wait until its API server and etcd member are healthy, then move to the next. Restarting every etcd member at the same time costs you quorum for as long as the slowest one takes to come back.

Any control-plane node can run the worker rebuild, since all of them share the same CA. The server: line in the worker’s kubelet.conf must keep pointing at the load balancer or VIP, never at one control-plane node directly. If you built the cluster from our kubeadm HA cluster guide, that is the VIP or HAProxy address on 6443. Our lab had a single control plane, so this section follows the kubeadm docs rather than a captured run.

How to stop it happening next year

Upgrades are the cheapest prevention, because kubeadm renews every control-plane certificate during one, and a cluster that follows the kubeadm upgrade process at least once a year never reaches its expiry date. Kubernetes ships a minor release roughly every four months, so a team that stays within support upgrades often enough by default. The clusters that hit this outage are the stable ones nobody wanted to touch, such as a home Kubernetes cluster that just worked for twelve months.

Beyond upgrades, these are the settings and checks that catch it early:

  • Expiry alert: openssl x509 -checkend 2592000 exits non-zero when a certificate expires within 30 days, so a daily cron job against apiserver.crt on each control-plane node is enough to page someone.
  • Kubelet metric: each kubelet exports kubelet_certificate_manager_client_ttl_seconds, and an alert on a low value catches a worker whose rotation has stalled before it goes NotReady.
  • Validity period: new clusters can set certificateValidityPeriod in the v1beta4 ClusterConfiguration (default 8760h). A longer value pushes the date out, but it also extends the life of a leaked admin credential, so treat it as a trade, not a fix.
  • Long-off nodes: a worker that is powered off past its certificate date always comes back broken, as worker01 did, so check its PEM with openssl before you boot it back into service.

We keep the cron check short enough to show in full. It prints a line only when the API server certificate has less than 30 days left:

sudo openssl x509 -checkend 2592000 -noout -in /etc/kubernetes/pki/apiserver.crt || echo "apiserver.crt expires within 30 days on $(hostname)"

Against the freshly renewed certificate it printed Certificate will not expire and exited 0, so the echo stayed silent. New clusters built with our kubeadm install guide for Ubuntu start that one-year clock on install day, so it’s worth putting this check in place the same afternoon. Renewing kubeadm certificates is also a common CKA practice task, and several of the Kubernetes certification books on our list walk through the PKI layout in more depth than the docs do.

Where this recovery breaks down

Everything above assumes the cluster CAs are still valid and their keys are on a control-plane node. When either assumption fails, kubeadm certs renew all has nothing to sign with and the recovery turns into a different job:

  • Expired CA: the CAs last ten years, and renewing leaf certificates against an expired CA produces certificates nobody trusts. Rotating the CA itself is a manual, multi-step process covered in the upstream manual-rotation docs.
  • External CA: if check-expiration marks a certificate EXTERNALLY MANAGED, kubeadm doesn’t have ca.key. Generate CSRs with kubeadm certs generate-csr and have whoever owns the CA sign them.
  • Kubelet serving certificate: the certificate the kubelet presents on port 10250 is self-signed by default, because serverTLSBootstrap is off. With it turned on, serving CSRs are not auto-approved, so each one needs kubectl certificate approve.

For the ordinary case, a year-old cluster that nobody upgraded, the order is what matters: check the clock, renew, restart the static pods, copy admin.conf, then rebuild each NotReady worker’s kubelet.conf and point it back at the rotating file. The upstream kubeadm certificate management page remains the reference for flags that change between releases.

Keep reading

Best UI Applications for Managing Docker Containers Containers Best UI Applications for Managing Docker Containers Install Docker and Run Containers on Ubuntu 24.04|22.04 Containers Install Docker and Run Containers on Ubuntu 24.04|22.04 Install UniFi OS Server on Ubuntu 24.04 LTS Containers Install UniFi OS Server on Ubuntu 24.04 LTS CapRover Docker Install on Debian 13: Deploy Your First App Containers CapRover Docker Install on Debian 13: Deploy Your First App Best OpenShift Books for Admins and EX280 Candidates Books Best OpenShift Books for Admins and EX280 Candidates Run Linux Containers with LXC/LXD on Ubuntu 24.04|22.04|20.04|18.04 Containers Run Linux Containers with LXC/LXD on Ubuntu 24.04|22.04|20.04|18.04

Leave a Comment

Press ESC to close