bhouse-nexthop opened a new pull request, #14651:
URL: https://github.com/apache/cloudstack/pull/14651

   ### 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 `getKubernetesClusterConfig` returns 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
   
   1. **Stored kubeconfig refresh.** `kubeConfigData` is read again from the 
control node:
      - after `upgradeKubernetesCluster` (a failure there is logged and doesn't 
fail the upgrade);
      - after every certificate rotation;
      - whenever the expiry check finds the stored copy's admin certificate 
expiring before the one on the cluster, e.g. after a renewal by hand.
   
      The read-and-rewrite code that `StartWorker` had is now 
`KubernetesClusterActionWorker.refreshKubeConfigData()`, shared by all three. 
`KubernetesClusterUtil.getKubernetesClusterConfig` now waits 5 seconds between 
attempts instead of retrying a refused SSH connection in a tight loop until its 
timeout.
   2. **Expiry check.** A new scanner, on its own thread next to 
`KubernetesClusterStatusScanner`, runs `kubeadm certs check-expiration` over 
SSH on every control node of each Running or Alert cluster, at most every 
`cloud.kubernetes.cluster.certificate.check.interval` hours. 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.
      - When the first leaf certificate reaches one of the 
`cloud.kubernetes.cluster.certificate.expiry.alert.days` thresholds, CKS raises 
an alert (new type `ALERT.KUBERNETES.CLUSTER.CERTIFICATE`) and records a 
`KUBERNETES.CLUSTER.CERTIFICATES.EXPIRY` event. Each threshold fires once per 
expiry date, and expiry itself alerts once more. A renewal moves the date, so 
the thresholds start over.
      - The first certificate authority to expire is tracked the same way, with 
its own alert. CAs are reported, never rotated.
      - When none of a cluster's control nodes can be checked, or the cluster 
has no address to reach it on, an alert says so, at most once a day, so an 
unreachable cluster can't expire unnoticed. Each cluster's state is read again 
just before it is checked, so a cluster stopped during a pass is skipped.
   3. **`rotateKubernetesClusterCertificates` API** (async, same roles as 
`upgradeKubernetesCluster`). For each control node, one at a time, it copies 
`rotate-kubernetes-certificates.sh` to the node and runs it. The script:
      - puts back any static pod manifest left aside by an interrupted earlier 
run;
      - fails before touching anything if `crictl` can't list the containers, 
since it needs it to confirm the static pods restarted;
      - waits up to 3 minutes for the local API server to be ready, because 
`kubeadm certs renew` silently 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;
      - runs `kubeadm certs renew all`, and fails if `apiserver.crt` or 
`admin.conf` did not change or kubeadm says a certificate "can't be renewed" 
(external CA mode, where kubeadm exits 0 without renewing anything);
      - copies `admin.conf` to `/root/.kube/config`. If that file was something 
else before the renewal, it is kept as `config.cks-backup`, and an existing 
backup is never overwritten;
      - moves the `kube-apiserver`, `kube-controller-manager`, `kube-scheduler` 
and `etcd` manifests out, waits until the kubelet has stopped those containers, 
then moves the manifests back. An exit trap moves them back on any failure too;
      - waits for `/readyz` on the API server, `/healthz` on 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-expiration` on any control node shows an 
expired certificate that the control plane runs on, the worker switches to 
rescue mode. An expired `admin.conf` or `super-admin.conf` breaks 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:
      - it renews every control node first, and on each node the script only 
waits for etcd (when the node runs one) to run again and for new sandboxes of 
the other control plane pods. The API server keeps restarting until etcd has 
quorum, so it can't wait for that;
      - then it waits for the cluster API and for every control node to be 
Ready.
   
      Rerunning is safe. `kubeadm certs renew` issues fresh certificates every 
time, and the interrupted-run recovery above makes the script pick up where a 
killed run left off.
   4. **Automatic renewal.** When the first renewable leaf certificate is 
within `cloud.kubernetes.cluster.certificate.auto.renew.days` and 
`cloud.kubernetes.cluster.certificate.auto.renew.enabled` is 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.
      - Certificates kubeadm reports as externally managed are left out of this 
decision, since renewing can't fix them and every check would renew again.
      - After a failed rotation, automatic renewal waits 24 hours before trying 
again; the failure has already raised an alert.
      - When a control node couldn't be checked, automatic renewal waits too: 
it would renew the reachable nodes and then fail on that one. The expiry alert 
then says renewal is on hold and names the nodes.
   5. **Guards.**
      - Rotation and the check need Kubernetes 1.20 or later, where `kubeadm 
certs` replaced `kubeadm alpha certs`. The API refuses older clusters and the 
scanner skips them.
      - The API refuses clusters that aren't Running or Alert, and externally 
managed clusters. Clusters in Alert are checked and alerted on, since one can 
sit there for long (a stopped worker is enough), but automatic renewal only 
runs on Running clusters.
      - A queued automatic renewal checks again when it gets a thread, and 
skips the cluster if it is no longer Running or was rotated meanwhile, 
successfully or not.
      - Clusters created before 4.16 have no control node flag in 
`kubernetes_cluster_vm_map`. For those, the control nodes are taken by 
position, after any etcd nodes, as `fetchControlVmIfMissing` already does.
      - `startKubernetesCluster` and `stopKubernetesCluster` refuse a cluster 
in `RotatingCertificates`, whose control plane manifests may be moved aside at 
that moment. Upgrade already requires Running, and scale Created, Running or 
Stopped.
      - A new transient state, `RotatingCertificates`, is entered from Running 
or Alert through the new `CertificateRotationRequested` event. 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 
from `RotatingCertificates`, and a rotation can't start while another operation 
runs.
      - The status scanner's Alert reconciliation could move a cluster to 
Running, or to Stopped, while a rotation from Alert had just started. 
`reconcileAlertCluster` and the all-VMs-stopped branch now stop when their own 
first transition fails, instead of applying `OperationSucceeded` regardless.
      - On management server shutdown, automatic renewals still waiting for a 
thread are dropped rather than started; the next check after the restart 
schedules them again.
      - If the management server running a rotation goes away, the cluster 
would stay in `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 past 
`cloud.kubernetes.cluster.certificate.rotation.timeout` (twice the timeout plus 
10 minutes). Either way an alert is raised.
   6. `upgrade-kubernetes.sh` now also copies the renewed `admin.conf` to 
`/root/.kube/config` on control nodes, so CKS's own kubectl keeps working after 
the old certificate expires. As in the rotation script, a `/root/.kube/config` 
that differed from `admin.conf` before 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 in 
`kubernetes_cluster_details`.
   
   #### Settings
   
   | Setting | Default | Scope | Meaning |
   |---|---|---|---|
   | `cloud.kubernetes.cluster.certificate.check.interval` | `6` | Global | 
Hours between expiry checks of a cluster. `0` disables the check and automatic 
renewal. |
   | `cloud.kubernetes.cluster.certificate.expiry.alert.days` | `60,30` | 
Global | Days before expiry at which to alert. |
   | `cloud.kubernetes.cluster.certificate.auto.renew.enabled` | `true` | 
Account | Renew automatically before expiry. |
   | `cloud.kubernetes.cluster.certificate.auto.renew.days` | `30` | Account | 
How many days before expiry to renew automatically. `0` disables it; values 
above 180 are treated as 180. |
   | `cloud.kubernetes.cluster.certificate.rotation.timeout` | `3600` | Global 
| Timeout of a rotation, in seconds. |
   
   All 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.
   - **Several control nodes:** the restart rolls one node at a time and etcd 
keeps quorum. The API stays up behind the load balancer. A few requests that 
the load balancer sends to the restarting node can fail before its health check 
takes the node out; clients retry them. In the three-node test below, 6 of 187 
polls through the load balancer failed, two per node.
   - **One control node:** the API is unavailable for roughly 30–40 seconds.
   - **Workloads keep running in both cases.** Only the control plane restarts; 
kubelets and pods don't.
   - **Kubeconfigs downloaded before the renewal** keep working until their old 
certificate expires. Renewal doesn't revoke the old certificate.
   
   #### External etcd
   
   `kubeadm certs renew` doesn't touch separate etcd nodes, but the etcd nodes 
CKS creates (`etcd-node.yml`) serve etcd over plain HTTP: `--listen-client-urls 
http://...`, peer URLs `http://...`, 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, so `check-expiration` and `renew` leave 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
   
   - CA rotation; CAs are only reported.
   - A UI button for the new API.
   - `/etc/kubernetes/user.conf`. CKS returns it instead of `admin.conf` when 
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 with `admin.conf`.
   - The cluster state update is not a compare-and-set 
(`KubernetesClusterDaoImpl.updateState` writes 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. Making `updateState` conditional, 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
   
   - [ ] Breaking change (fix or feature that would cause existing 
functionality to change)
   - [x] New feature (non-breaking change which adds functionality)
   - [x] Bug fix (non-breaking change which fixes an issue)
   - [ ] Enhancement (improves an existing feature and functionality)
   - [ ] Cleanup (Code refactoring and cleanup, that may add test cases)
   - [ ] Build/CI
   - [ ] Test (unit or integration test code)
   
   ### Feature/Enhancement Scale or Bug Severity
   
   #### Feature/Enhancement Scale
   
   - [ ] Major
   - [x] Minor
   
   #### Bug Severity
   
   - [ ] BLOCKER
   - [x] Critical
   - [ ] Major
   - [ ] Minor
   - [ ] Trivial
   
   ### How Has This Been Tested?
   
   #### Unit tests
   
   All 189 tests of the CKS plugin pass, including these new ones.
   - `KubernetesClusterCertificateUtilTest`:
     - parsing real `kubeadm certs check-expiration` output (v1.31): both 
tables, missing, expired and externally managed certificates, and error output;
     - reading a kubeconfig's client certificate expiry;
     - the threshold parsing and alert decision (one alert per threshold per 
expiry date; a cluster first seen inside both windows alerts once; expiry 
alerts once more);
     - the auto-renew decision.
   - `KubernetesClusterCertificateWorkerTest`:
     - the rolling sequence: node by node, renew then wait for ready, then the 
kubeconfig refresh and Running;
     - stopping at the first node that fails to renew or doesn't become ready, 
with the remaining nodes untouched, Alert and an alert;
     - rescue mode when a certificate on any control node has expired (every 
node renewed first, then each waited for), and the node-by-node path otherwise;
     - a rotation from Alert going back to Alert;
     - a second rotation of the same cluster refused without disturbing the 
first, and a cluster another server moved out of this rotation meanwhile left 
in its state;
     - `/root/.kube/config` treated as externally managed when `admin.conf` is 
(external CA mode), so it doesn't trigger renewal;
     - failure events recorded only for automatic renewals, since the API's 
async job records its own;
     - a failed kubeconfig refresh;
     - refusal when the state transition fails and before 1.20;
     - the per-node SSH endpoint (direct network, the cluster IP's port 
forwarding rule rather than a user's rule on another IP, and by position);
     - the check across control nodes with one unreachable;
     - refreshing a stale stored kubeconfig, and comparing it with `user.conf` 
when there is one.
   - `KubernetesClusterCertificateManagementTest`:
     - the scanner: at most one check per interval, the 60 and 30 day alerts 
firing once each as expiry approaches, thresholds starting over after a 
renewal, CA alerts without rotation, Alert clusters checked but not renewed, no 
automatic renewal while a control node is unreachable (and the alert saying 
so), the daily alert when no node can be checked or the cluster can't be 
reached at all, the alert advice using the effective renewal days, auto-renew 
on and off and handed to the rotation thread pool (queued once, requeued after 
a rejection, skipped when renewed meanwhile), the 24 hour back-off after a 
failure, externally managed certificates not triggering renewal, and the 
version gate;
     - moving an interrupted rotation to Alert: at once when it belongs to this 
management server, otherwise only once it is stale, and never a rotation that 
just finished;
     - the API validation (Running and Alert accepted) and the new state 
machine transitions.
   - `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.sh` on throwaway kind clusters 
(Kubernetes v1.31, kubeadm with stacked etcd):
   - **Single control node:**
     - certificates renewed, all four static pods restarted with new container 
IDs, the API server serving the renewed certificate, `/root/.kube/config` 
identical to the new `admin.conf`, the node Ready. About 40 seconds.
     - `ca.key` moved 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.
     - Killed half-way, with the `kube-apiserver` and `etcd` manifests 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.crt` hidden so `kubeadm certs renew all` itself fails: the script 
prints kubeadm's error, exits 1, and leaves all four manifests in place.
     - API server unreachable (port 6443 rejected inside the node, standing in 
for expired certificates): the script waits 3 minutes, warns, renews, restarts 
the static pods and completes once the API server is reachable again.
   - **Rescue of a three-node cluster with no etcd quorum.** I rejected the 
etcd peer port on all three nodes, standing in for expired peer certificates. 
That leaves etcd without quorum and the API servers restarting. This confirmed 
the review's point: a node-by-node wait can't get past the first node.
     - In rescue mode, each node took about 40 seconds, and the script exited 0.
     - Once the peer port was open again, `/readyz` returned ok and all three 
nodes were Ready. Each API server served its renewed certificate, and etcd 
reported all three members healthy.
     - Without the cluster's API, kubeadm renews with its default 
configuration. It takes the certificate attributes (CN, SANs) from the existing 
certificates, so they come out the same.
   - A custom `/root/.kube/config` survived two renewals in a row as 
`config.cks-backup`, and the readiness check matched a cordoned control node.
   - **Three control nodes behind kind's load balancer:** renewed node by node, 
waiting for each node to be Ready with the same `kubectl get nodes` check the 
worker uses, while polling `/readyz` through the load balancer every 0.5 s.
     - Every node served its renewed certificate afterwards, and etcd reported 
all three members healthy.
     - 6 of 187 load balancer polls failed, two per node while its API server 
restarted.
   
   I also fed the `check-expiration` output and the `/root/.kube/config` 
certificate 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.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to