Repository navigation
CKS: check and renew the control plane certificates - #14651
bhouse-nexthop wants to merge 2 commits into
Conversation
kubeadm issues the control plane certificates of a CKS cluster for one year, and nothing in CKS ever renewed them or looked at their expiry. CKS also only refreshed the kubeconfig that getKubernetesClusterConfig returns on start, so even after an upgrade, which renews the certificates, it kept handing out the old admin certificate until it expired. - Refresh the stored kubeconfig after an upgrade and a renewal, and whenever the expiry check finds it older than the cluster's admin.conf. - Check the certificate expiry of every control node of running clusters with kubeadm certs check-expiration, at most every cloud.kubernetes.cluster.certificate.check.interval hours (6), on a scanner thread of its own. Raise an alert and an event as the first certificate, and separately the first certificate authority, reach each of cloud.kubernetes.cluster.certificate.expiry.alert.days (60,30). - Add the rotateKubernetesClusterCertificates API. It renews the certificates on one control node at a time, restarts that node's control plane static pods, waits for its API server and the node to be ready, stops at the first node that fails and raises an alert, then refreshes the stored kubeconfig. The cluster is RotatingCertificates meanwhile, so no other operation can start. A cluster in Alert can be rotated too, to retry a failed rotation or to rescue a cluster whose certificates already expired: when a certificate on any control node has expired, every control node is renewed before waiting, since stacked etcd has no quorum until they all are. - Renew automatically cloud.kubernetes.cluster.certificate.auto.renew.days (30) before expiry when cloud.kubernetes.cluster.certificate.auto.renew.enabled (true) is set for the cluster's account, through the same rolling path. - Keep root's kubeconfig on control nodes, which CKS itself uses for kubectl, in step with admin.conf after an upgrade or a renewal. Certificate authorities are reported but not rotated. Clusters older than Kubernetes 1.20, which have no kubeadm certs command, are skipped. External etcd nodes serve plain HTTP and hold no certificates, so only control nodes are renewed. Signed-off-by: Brad House <bhouse@nexthop.ai>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #14651 +/- ##
=========================================
- Coverage 3.88% 3.87% -0.01%
=========================================
Files 493 493
Lines 42580 42609 +29
Branches 8072 8076 +4
=========================================
Hits 1653 1653
- Misses 40696 40725 +29
Partials 231 231
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Test report: CKS control plane certificate check, rotation and automatic renewal, on live clustersSummary:
Environment
Results
First check after the upgrade
Alert thresholds
Rotation of A, with the API polled
Rotation of B, with the API polled
Guards
Kubeconfig after an upgrade
Automatic renewal, without changing the clock
Result: pass.
Not covered
Notes
|
It has a shebang, and pre-commit's check-shebang-scripts-are-executable rejects it otherwise. Signed-off-by: Brad House <bhouse@nexthop.ai>
|
@bhouse-nexthop , you reported this one to be present on 4.22. care to rebase? |
I could but I thought 4.22 didn't take new features. This adds a new API for this and whatnot so I was just targeting 4.24 for this. I've actually got a huge queue of CKS fixes for 4.24. |
Description
kubeadm issues the control plane certificates of a CKS cluster for one year. CKS never renewed them and never checked their expiry, so a cluster that isn't upgraded within a year stops working when they expire. And because CKS only read the kubeconfig that
getKubernetesClusterConfigreturns when the cluster started, even an upgrade, which renews the certificates, left it handing out the old admin certificate until that expired.This PR checks the expiry, alerts ahead of it, adds an API to renew the certificates one control node at a time, renews them automatically before they expire, and refreshes the stored kubeconfig after every upgrade and renewal.
Fixes #14650
What changed
Stored kubeconfig refresh.
kubeConfigDatais read again from the control node:upgradeKubernetesCluster(a failure there is logged and doesn't fail the upgrade);The read-and-rewrite code that
StartWorkerhad is nowKubernetesClusterActionWorker.refreshKubeConfigData(), shared by all three.KubernetesClusterUtil.getKubernetesClusterConfignow waits 5 seconds between attempts instead of retrying a refused SSH connection in a tight loop until its timeout.Expiry check. A new scanner, on its own thread next to
KubernetesClusterStatusScanner, runskubeadm certs check-expirationover SSH on every control node of each Running or Alert cluster, at most everycloud.kubernetes.cluster.certificate.check.intervalhours. It also reads the client certificate of/root/.kube/config, which CKS uses for its own kubectl calls. The scanner wakes every 15 minutes, but the per-cluster interval is tracked in a cluster detail, so a cluster is checked once per interval whichever management server runs it, and an unreachable cluster isn't retried until the next interval.cloud.kubernetes.cluster.certificate.expiry.alert.daysthresholds, CKS raises an alert (new typeALERT.KUBERNETES.CLUSTER.CERTIFICATE) and records aKUBERNETES.CLUSTER.CERTIFICATES.EXPIRYevent. Each threshold fires once per expiry date, and expiry itself alerts once more. A renewal moves the date, so the thresholds start over.rotateKubernetesClusterCertificatesAPI (async, same roles asupgradeKubernetesCluster). For each control node, one at a time, it copiesrotate-kubernetes-certificates.shto the node and runs it. The script:crictlcan't list the containers, since it needs it to confirm the static pods restarted;kubeadm certs renewsilently falls back to its default configuration when it can't read the cluster's. If it still isn't ready, the script warns and renews anyway: once the certificates have expired the API server never becomes ready, and renewing is the way out;kubeadm certs renew all, and fails ifapiserver.crtoradmin.confdid not change or kubeadm says a certificate "can't be renewed" (external CA mode, where kubeadm exits 0 without renewing anything);admin.confto/root/.kube/config. If that file was something else before the renewal, it is kept asconfig.cks-backup, and an existing backup is never overwritten;kube-apiserver,kube-controller-manager,kube-schedulerandetcdmanifests out, waits until the kubelet has stopped those containers, then moves the manifests back. An exit trap moves them back on any failure too;/readyzon the API server,/healthzon the controller manager and scheduler, and for the API server to serve the renewed certificate.Once the script is done on a node, the worker waits for the cluster API endpoint to answer and the node to be Ready before it moves to the next node. The shared readiness check now also accepts
Ready,SchedulingDisabled, so a cordoned control node doesn't fail the rotation. It stops at the first node that fails, moves the cluster to Alert and raises an alert. After the last node it refreshes the stored kubeconfig.A failed rotation leaves the cluster in Alert, and the API accepts Alert as well as Running, so it can be retried right away. A rotation started from Alert goes back to Alert when it succeeds. Whatever else put the cluster there hasn't been checked, so the status scanner moves it to Running once its nodes are ready.
The API also rescues a cluster whose certificates have already expired. Its API server no longer answers, so it is in Alert. With stacked etcd and several control nodes, one renewed node can't get etcd quorum until the others are renewed too, so waiting for each node in turn would never get past the first. When
kubeadm certs check-expirationon any control node shows an expired certificate that the control plane runs on, the worker switches to rescue mode. An expiredadmin.conforsuper-admin.confbreaks no component and doesn't count. Checking every node matters: after an interrupted rescue the first node is renewed while the others still block quorum. The decision goes by expiry, not by whether the API answers. An expired API server that is still running keeps answering/version, while a healthy cluster that is briefly unreachable must keep the node-by-node path, since restarting every control node at once would cost it etcd quorum. In rescue mode:Rerunning is safe.
kubeadm certs renewissues fresh certificates every time, and the interrupted-run recovery above makes the script pick up where a killed run left off.Automatic renewal. When the first renewable leaf certificate is within
cloud.kubernetes.cluster.certificate.auto.renew.daysandcloud.kubernetes.cluster.certificate.auto.renew.enabledis true for the cluster's account, the scanner hands the same rotation to a small thread pool, so a renewal that takes minutes doesn't hold up the checks of other clusters. When it is done, an event tells users to download the kubeconfig again. Values above 180 days are treated as 180, so a mistyped setting can't renew every cluster on every check.Guards.
kubeadm certsreplacedkubeadm alpha certs. The API refuses older clusters and the scanner skips them.kubernetes_cluster_vm_map. For those, the control nodes are taken by position, after any etcd nodes, asfetchControlVmIfMissingalready does.startKubernetesClusterandstopKubernetesClusterrefuse a cluster inRotatingCertificates, whose control plane manifests may be moved aside at that moment. Upgrade already requires Running, and scale Created, Running or Stopped.RotatingCertificates, is entered from Running or Alert through the newCertificateRotationRequestedevent. It goes back to Running on success and to Alert on failure. As with upgrade and scale, the state transition is what keeps other operations out: none of them can start fromRotatingCertificates, and a rotation can't start while another operation runs.reconcileAlertClusterand the all-VMs-stopped branch now stop when their own first transition fails, instead of applyingOperationSucceededregardless.RotatingCertificates. The rotation records which management server runs it. When that server comes back, its scanner moves the rotation to Alert straight away; any other server does so once it is well pastcloud.kubernetes.cluster.certificate.rotation.timeout(twice the timeout plus 10 minutes). Either way an alert is raised.upgrade-kubernetes.shnow also copies the renewedadmin.confto/root/.kube/configon control nodes, so CKS's own kubectl keeps working after the old certificate expires. As in the rotation script, a/root/.kube/configthat differed fromadmin.confbefore the upgrade is kept as a backup first.No schema changes. The state column is
char(32), and the bookkeeping (last check, alerted thresholds, rotation start and last failure) is kept inkubernetes_cluster_details.Settings
cloud.kubernetes.cluster.certificate.check.interval60disables the check and automatic renewal.cloud.kubernetes.cluster.certificate.expiry.alert.days60,30cloud.kubernetes.cluster.certificate.auto.renew.enabledtruecloud.kubernetes.cluster.certificate.auto.renew.days300disables it; values above 180 are treated as 180.cloud.kubernetes.cluster.certificate.rotation.timeout3600All of them are dynamic.
Automatic renewal is on by default. A plain kubeadm cluster with no upgrades and no renewal timer is the one setup where the certificates simply run out. RKE2 and k3s renew on restart, and managed services rotate them unseen. Renewing 30 days ahead leaves a month for people and CI that downloaded the old kubeconfig to fetch it again, and the event says so. Anyone who wants to schedule the restarts themselves can turn it off globally or per account and use the API.
Restart impact
Static pods don't reload certificates, so each control node's API server, controller manager, scheduler and stacked etcd restart. One node takes about 40 seconds.
External etcd
kubeadm certs renewdoesn't touch separate etcd nodes, but the etcd nodes CKS creates (etcd-node.yml) serve etcd over plain HTTP:--listen-client-urls http://..., peer URLshttp://..., no--cert-file. They hold no certificates, so there is nothing to renew or check on them, and the rotation only touches control nodes. kubeadm sees those clusters as having external etcd, socheck-expirationandrenewleave out the etcd certificates there and handle the rest normally.If etcd nodes ever get TLS, they'll need their own renewal step. Unencrypted etcd traffic is a separate concern and out of scope here.
Not in this PR
/etc/kubernetes/user.conf. CKS returns it instead ofadmin.confwhen an operator has put one on the control node. It is the operator's file, possibly signed by something CKS doesn't manage, so CKS neither renews nor rewrites it. The stale-kubeconfig check compares the stored copy with it rather than withadmin.conf.KubernetesClusterDaoImpl.updateStatewrites unconditionally). That's how every CKS operation works today, and this PR uses the same guard. Likewise, the existing workers carry on when their own first transition fails. An upgrade validated as Running just before an automatic renewal takes the cluster could therefore still run alongside it; the window is the time between the API's state check and the worker's transition. MakingupdateStateconditional, and having every worker stop when its first transition fails, would close this race for all operations. That's a separate change.Types of changes
Feature/Enhancement Scale or Bug Severity
Feature/Enhancement Scale
Bug Severity
How Has This Been Tested?
Unit tests
All 189 tests of the CKS plugin pass, including these new ones.
KubernetesClusterCertificateUtilTest:kubeadm certs check-expirationoutput (v1.31): both tables, missing, expired and externally managed certificates, and error output;KubernetesClusterCertificateWorkerTest:/root/.kube/configtreated as externally managed whenadmin.confis (external CA mode), so it doesn't trigger renewal;user.confwhen there is one.KubernetesClusterCertificateManagementTest:KubernetesClusterUpgradeWorkerTest: the stored kubeconfig is refreshed after a successful upgrade, before the cluster goes back to Running; a failed refresh doesn't fail the upgrade; a failed upgrade doesn't refresh.The renewal script on real kubeadm control planes
I ran
rotate-kubernetes-certificates.shon throwaway kind clusters (Kubernetes v1.31, kubeadm with stacked etcd):/root/.kube/configidentical to the newadmin.conf, the node Ready. About 40 seconds.ca.keymoved away (external CA mode): kubeadm exits 0 and renews nothing it needs the CA key for. The script fails before restarting anything, and the API server container is unchanged.kube-apiserverandetcdmanifests left aside and the API server down: the rerun puts them back, waits for the API server, renews with the cluster's own kubeadm configuration rather than the fallback defaults, and completes.ca.crthidden sokubeadm certs renew allitself fails: the script prints kubeadm's error, exits 1, and leaves all four manifests in place./readyzreturned ok and all three nodes were Ready. Each API server served its renewed certificate, and etcd reported all three members healthy./root/.kube/configsurvived two renewals in a row asconfig.cks-backup, and the readiness check matched a cordoned control node.kubectl get nodescheck the worker uses, while polling/readyzthrough the load balancer every 0.5 s.I also fed the
check-expirationoutput and the/root/.kube/configcertificate line from those nodes through the Java parser, and the expiry dates matched.I haven't run a full CKS cluster through the new API and the scanner end to end. The Java orchestration around the script is covered by the unit tests above.