Cluster Administration
Day-2 operations. Every command here is a subcommand of the same single binary that installed the cluster — there is no separate operator CLI to distribute.
| High Availability | three controllers, a VIP, and how etcd membership is managed |
| Upgrades & Rollback | rolling upgrades with the skew rules enforced, and how to go back |
| Snapshot & Restore | scheduled etcd snapshots, restore, and rejoining a member |
| Hardening | the CIS profile and the hardening check verifier |
| MCP Server | serving these tools to an AI agent, and the tiers that bound it |
Certificates
sudo stratum certs check # per-leaf expiry table + shared-cert-key age
sudo stratum certs rotate --yes # reissue the leaves from the existing CAs
sudo stratum certs rotate --rekey-shared --yes # re-key the HA shared cert material
sudo stratum certs rotate-ca --cluster --yes # the heavier cluster-wide CA rotation
sudo stratum certs migrate-etcd-ca --cluster --yes # move a pre-existing cluster onto the etcd CA
sudo stratum certs migrate-kubelet-client-ca --cluster --yes # move a pre-existing cluster onto the kubelet client CA
Every command here except certs check needs --yes. Without it, the command
refuses and says what it would disrupt.
rotate-ca, migrate-etcd-ca and migrate-kubelet-client-ca also need an
operation selector. --cluster runs every phase across the cluster: on every
node for rotate-ca and migrate-kubelet-client-ca, and on every control plane
for migrate-etcd-ca, because workers run no etcd. Start a
--cluster run from a control-plane node; on a worker, the command refuses.
--phase=<name> runs one phase on the current node only. Use --cluster. The
--phase form is what the cluster run executes on each node, and you need it
only to recover a halted run by hand.
To undo a failed run, use --rollback instead. rotate-ca --rollback reverts
the current node only, even with --cluster. migrate-etcd-ca --rollback
reverts the current node, or every control plane with --cluster.
migrate-kubelet-client-ca --rollback needs --cluster and reverts every node. Without
--cluster, --phase or --rollback, the command refuses and lists the phases.
Leaf certificates are issued for 365 days. Each control plane checks its
leaves daily and rotates any within 45 days of expiry, so certs rotate is for
when you want it to happen now rather than eventually. To turn the automatic
rotation off, set spec.certRenew: false in /etc/stratum/config.yaml; it is
on by default.
certs check also reports when the HA shared-cert certificate-key was last set.
That key encrypts the stratum-certs Secret and only re-keys as a side effect of
token create --control-plane or certs rotate-ca, so an install-once cluster can
carry its install-time key indefinitely. certs rotate --rekey-shared mints a fresh
key and re-encrypts the shared material on demand — after it runs, the previous key
(printed to stdout on builds before the file-handover change) no longer decrypts
anything. The new key is written to a 0600 file whose path is printed, never the
key itself; existing control planes need no action.
certs rotate-ca is phased — it widens trust, reissues under the new authority,
then narrows — so a cluster is never in a state where one node trusts an authority
another does not.
On a cluster that runs operators or other workloads that call the API server,
rotate-ca --cluster --yes now refuses to start and lists those workloads.
Scripts that ran it unattended must add --restart-workloads or
--skip-workload-check. See
Workloads that call the API server.
Every --cluster run now checks the service-account tokens mounted on each
node before finalize, then waits 2 minutes. If a node still mounts a token
signed by the old key, the run keeps checking for up to 60 minutes and then
halts before finalize. See Before finalize: mounted tokens.
The API server verifies each kubelet's serving certificate against the cluster
CA, without --cis too. sign-new now renews every worker's serving
certificate under the new CA, and a CIS control plane renews its own at the
start of its finalize. The operator approves these certificates. Without
it, a new worker cannot obtain a serving certificate automatically; an
existing worker keeps its issued certificate but cannot renew it
automatically. sign-new refuses if the serving certificate is missing
or its renewal CSR remains unapproved.
See Disable the operator.
rotate-ca --cluster refuses to finalize when a non-CIS control-plane
kubelet cannot be verified against the new CA or when a reachable worker
still serves a certificate it did not sign. CIS control planes renew and
restart their kubelet during their own finalize. A worker's finalize
refuses when its serving certificate is missing, unreadable or still
signed by the old CA.
A node that finalized a rotation on an earlier release can still serve a certificate from the CA that rotation dropped; see Nodes that finalized a CA rotation on an earlier build.
A cluster created by this release has a fourth CA, kubelet-client-ca,
and its kubelets trust it alone, so a certificate the cluster root signs,
admin's included, no longer reaches a kubelet's API (see
The kubelet's client trust).
apiserver-kubelet-client is now authorized through RBAC rather than
system:masters (see The apiserver's kubelet client).
- A cluster created by an earlier release keeps the cluster root as its
kubelets' client trust until you run
certs migrate-kubelet-client-ca. Until thenhardening checkreportskubelet.1FAIL on every node andlive.kubeletFAIL on a control plane, so a script that gates on its exit status fails on such a cluster. - Join a cluster created by this release only with nodes of this release or later.
certs rotate-carotateskubelet-client-cawith the cluster root, and refuses to start on a node part way through the migration.
stratum certs rotate-ca --cluster, stratum certs migrate-etcd-ca --cluster
and stratum certs migrate-kubelet-client-ca --cluster now handle SIGHUP like SIGINT and SIGTERM: the first signal requests a stop
at the next step boundary and allows cleanup to run. Previously, SIGHUP
terminated these commands immediately. Follow the printed recovery guidance
before resuming a stopped rotation. A second signal forces termination;
when a shell or nohup supplied an ignored signal disposition, Stratum uses
SIGKILL for that force-stop.
Restart workloads after rotate-ca
The phases protect Stratum's own components, not your workloads. A pod that talks to the API server through in-cluster config (operators, cert-manager, ingress controllers, CoreDNS, Cilium) can fail in two ways during a rotation:
- From
sign-new, a cached CA causesx509errors. The API server starts serving a certificate from the new CA. The pod's mountedca.crtalready holds both CAs, but most clients load the CA pool once, at startup, and never re-read it. Every API call fails withx509: certificate signed by unknown authorityuntil the pod restarts. This does not clear by itself, and a controller stuck in leader election never exits, so Kubernetes never restarts it for you. - After
finalize, an old-key token causes401errors. Fromsign-new, the API server signs new service-account tokens with the new key, and it accepts tokens signed by either key untilfinalizedrops the old one. A pod restarted aftersign-newholds a new-key token and is unaffected. A pod still holding an old-key token atfinalize, because it was never restarted or because its client caches the token, gets401 Unauthorized.rotate-carefuses to finalize while a pod still mounts an old-key token. See Beforefinalize: mounted tokens.
Pods that do not call the API keep serving traffic, but controllers, DNS records for new Services, ingress route changes and certificate renewal all stop until the affected workloads restart.
The restart that clears the x509 errors has to happen after the new CA is
published and before sign-new. rotate-ca checks for it, and refuses until
you choose who does it. See
Workloads that call the API server.
--cluster --restart-workloads: Stratum restarts the workloads betweentrust-newandsign-new. You restart nothing forx509.--cluster --skip-workload-check: Stratum restarts only its own add-ons. Restart your workloads once, when the command completes. They are degraded fromsign-newuntil then.--phase: restart the workloads aftertrust-newhas completed on every node and before you runsign-new. Otherwisesign-newrefuses.
A pod restarted before sign-new is issued an old-key token. A client that
re-reads its token file picks up a new-key token when the kubelet refreshes it,
so it needs no restart after sign-new, provided it was restarted before it. A
workload left to you with --skip-workload-check still needs its restart, for
x509. Two cases still get 401 after finalize:
-
Clients that cache their service-account token in memory keep the old-key token they started with, even after a restart before
sign-new. Restart them aftersign-newhas completed on every control plane and beforefinalize. Thefinalizecheck reads mounted token files, so it cannot see these clients.--clusterrunsfinalizeitself, so to restart them in that window, run the phases with--phase. Otherwise restart them when--clustercompletes; they get401until then. -
Long-lived tokens from
kubernetes.io/service-account-tokenSecrets, and kubeconfigs built from them (CI/CD credentials, Argo CD cluster Secrets), were signed with the old key and never rotate. They get401afterfinalize, and restarting the pod that mounts them does not help. Delete and recreate each Secret so it is issued a token under the new key, then update any kubeconfig built from it. To list them:kubectl get secrets -A --field-selector type=kubernetes.io/service-account-token
rotate-ca prints a warning before it starts. Which one depends on the path.
rotate-ca --cluster prints:
WARNING: workloads that use in-cluster API config (operators, cert-manager, ingress controllers, Cilium)
keep the CA they started with, so from sign-new they fail with x509 errors until restarted.
Stratum restarts its own add-ons before sign-new. Your Deployments, DaemonSets and StatefulSets
are restarted before sign-new with --restart-workloads; with --skip-workload-check they are
not, and you restart them once, when this command completes.
A client that re-reads its service-account token needs no restart after sign-new, if it was
restarted before it.
Finalize refuses while any node still mounts a token signed with the old key. That scan reads
mounted token files, so it cannot see a client that caches its token in memory: that client
holds an old-key token even after a restart before sign-new, and is rejected with 401 once
finalize drops the key. Restart it after sign-new has completed on every control plane and
before finalize. This command runs finalize itself, so use --phase runs for that window;
otherwise restart it when this command completes.
Tokens from kubernetes.io/service-account-token Secrets do not rotate: recreate those
Secrets (and any kubeconfig built from them) after finalize; a restart does not help.
rotate-ca --phase prints:
WARNING: workloads that use in-cluster API config (operators, cert-manager, ingress controllers, Cilium)
keep the CA they started with, so from sign-new they fail with x509 errors until restarted.
Restart all of them, Stratum's own add-ons included, after trust-new has completed on every
node and before sign-new; sign-new refuses until they have, unless you pass --skip-workload-check.
A client that re-reads its service-account token needs no restart after sign-new, if it was
restarted before it.
A client that caches its service-account token in memory holds an old-key token even after
that restart, and is rejected with 401 once finalize drops the key. Restart it after sign-new
has completed on every control plane and before finalize.
--phase=finalize on a control plane refuses while that node mounts a token signed with the
old key. It checks that control plane only, and finalize on a worker does not check: scan
every node before the first control plane finalizes. The scan reads mounted token files, so
it does not see a client that caches its token in memory.
Tokens from kubernetes.io/service-account-token Secrets do not rotate: recreate those
Secrets (and any kubeconfig built from them) after finalize; a restart does not help.
Under --cluster, each node's Job runs rotate-ca --phase through the same
CLI, so every node's Job log also carries the --phase warning. It is expected
there, does not mean the phase failed, and does not change what you do: follow
the --cluster warning. A successful Job is
deleted. A failed Job is left in place, and the error rotate-ca --cluster
prints ends with the exact command to read its log, of the form
kubectl -n kube-system logs job/stratum-carotate-<phase>-<node>. Job names are
cut to 63 characters, so on a long node name the real name is shorter than that
pattern. Copy it from the error, or list the Jobs with
kubectl -n kube-system get jobs -l stratum.ciq.com/carotate.
To recover, do a rolling restart of every Deployment, DaemonSet and StatefulSet that uses in-cluster config:
kubectl -n kube-system rollout restart daemonset/cilium deployment/cilium-operator deployment/coredns
kubectl -n cert-manager rollout restart deployment
kubectl -n <namespace> rollout restart deployment/<name> # your operators, ingress controller, API clients
To find the workloads you missed, grep recent pod logs for x509 and 401.
In the cluster where this was measured (one control plane, about 40 pods), every
restarted pod was Ready within 28 seconds, with no x509 or 401 errors
afterwards.
Workloads that call the API server
rotate-ca counts a pod as an API client when both of these are true:
- A container mounts a service-account token: a projected volume with a
service-account token source (the
kube-api-access-*volume every pod gets, or any other name), or akubernetes.io/service-account-tokenSecret. The container can be a regular, init or ephemeral one; a volume that no container mounts does not count. - The ServiceAccount of that token is a subject of a RoleBinding or
ClusterRoleBinding. That is the pod's own ServiceAccount for a projected
token, and the account a
kubernetes.io/service-account-tokenSecret was issued for (itskubernetes.io/service-account.nameannotation), which can be another's. It is bound by name (as a ServiceAccount subject, or as the usersystem:serviceaccount:<namespace>:<name>) or through thesystem:serviceaccounts:<namespace>group. A subject of the cluster-widesystem:serviceaccountsgroup does not count: Kubernetes binds that group itself, so it would select every pod.
Finished pods are ignored. Pods that are being deleted still count, because their containers keep running on the old CA until the grace period ends. Pods are grouped by the controller that owns them, so a Deployment's ReplicaSet pods are reported as the Deployment. Jobs and bare Pods are reported, but Stratum never restarts them: recreate them yourself.
A pod's owner reference is written by whoever creates the pod, so Stratum only believes it when the Deployment, DaemonSet or StatefulSet it names exists with the same UID and has a valid name. A pod whose reference fails that is reported as the pod itself, and never restarted. Names that are not plain are printed quoted, and the commands in the listings are quoted for a shell.
| Flag | Applies to | Effect |
|---|---|---|
--restart-workloads | --cluster | Stratum restarts the Deployments, DaemonSets and StatefulSets it found, after the new CA is published and before sign-new, and waits for them to roll out. |
--skip-workload-check | --cluster, --phase=sign-new | Turns off both checks below. Nothing of yours is restarted, and those workloads fail with x509 errors from sign-new until you restart them. |
The command refuses before it changes anything if you pass:
- both flags:
--restart-workloads and --skip-workload-check contradict each other: pass one --restart-workloadswithout--cluster:--restart-workloads goes with --cluster
Before trust-new (--cluster)
Before the first phase, rotate-ca --cluster lists the API clients. If it finds
any and you passed neither flag, it refuses to start and changes nothing:
refusing to start: 1 workload(s) use in-cluster API credentials, and unless they restart after the new CA is published (trust-new) and before sign-new they fail with x509 errors:
Deployment apipoller/apipoller: stratum kubectl -n apipoller rollout restart deployment/apipoller
Pass --restart-workloads to have Stratum restart the Deployments, DaemonSets and StatefulSets among them (Jobs and bare Pods are never restarted), or --skip-workload-check to rotate without this check and restart them yourself
The check runs this early because a run that halts while trust-new is still
running on some node can only be rolled back. Stratum's own add-ons are not
listed here, because the run always restarts them.
Two more checks run before that one, with or without --restart-workloads.
They cover Stratum's own add-ons as well as your workloads, so they can refuse
even when you have no workloads of your own. Each refuses to start and changes
nothing, because a restart cannot fix the cause and the run would otherwise halt
after trust-new.
A StatefulSet or DaemonSet that a restart does not reach whole. A rollout restart leaves some or all of its pods on the old CA when:
- a StatefulSet has a
rollingUpdatepartition, which leaves the pods below it; - a StatefulSet or DaemonSet has the
OnDeleteupdate strategy, which does not recreate pods on a restart.
The refusal lists each workload with its reason, then the fix: one line for Stratum's own add-ons and one for your workloads, as they apply:
refusing to start: 2 workload(s) are not restarted whole by Stratum, so their pods stay on the old CA and the run would halt after trust-new, to be finished by hand:
StatefulSet db/postgres: a rollingUpdate partition leaves the pods below it
DaemonSet kube-system/cilium: the OnDelete update strategy does not recreate pods on a restart
Switch the update strategy of Stratum's own add-ons to RollingUpdate and run this again: Stratum restarts them whatever the flags say, and waits for a rollout that OnDelete never completes
Set the partition to 0 or the update strategy to RollingUpdate for your own workloads, or pass --skip-workload-check to rotate without this check and restart them yourself
Set the partition to 0, or switch the update strategy to RollingUpdate, then
run rotate-ca --cluster again. For Stratum's add-ons, such as Cilium,
switching to RollingUpdate is the only fix: Stratum restarts them and waits for
their rollout whatever the flags say, and an OnDelete rollout never completes,
so --skip-workload-check does not help.
# a StatefulSet with a partition
stratum kubectl -n db patch statefulset postgres \
-p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":0}}}}'
# a StatefulSet or DaemonSet with OnDelete
stratum kubectl -n kube-system patch daemonset cilium \
-p '{"spec":{"updateStrategy":{"type":"RollingUpdate"}}}'
If one of your own workloads needs to keep the partition or OnDelete, pass
--skip-workload-check instead, and replace its pods yourself between
trust-new and sign-new:
-
For a StatefulSet with a partition, restart it, wait for the rollout, then delete the pods below the partition. With a partition of 1, that is
postgres-0:stratum kubectl -n db rollout restart statefulset/postgres stratum kubectl -n db rollout status statefulset/postgres stratum kubectl -n db delete pod postgres-0 -
For an
OnDeleteStatefulSet or DaemonSet of yours, delete every pod of it.
A kube-root-ca.crt written in the future. Stratum tells a restarted pod from
a stale one by comparing its start time with the time kube-controller-manager
last wrote the namespace's kube-root-ca.crt, read from the ConfigMap's
managedFields. A namespace admin can set those fields. If the time is more than
5 minutes ahead of this host's clock, every pod in the namespace would count as
stale however often it restarts. Every namespace with an API client is checked,
including one whose only clients are Stratum's own add-ons, such as
kube-system:
refusing to start: apipoller/kube-root-ca.crt was written by kube-controller-manager at 2026-10-09T20:00:00Z, more than 5m0s ahead of this host's clock; its managedFields are not to be trusted (a namespace admin can set them), so pods cannot be told apart by start time
Delete that ConfigMap so kube-controller-manager recreates it, or pass --skip-workload-check to rotate without this check
Delete the ConfigMap. kube-controller-manager recreates it with a current write
time. Then run rotate-ca --cluster again:
stratum kubectl -n apipoller delete configmap kube-root-ca.crt
If you pass --skip-workload-check instead, Stratum does not check whether your
workloads restarted, so restart them yourself between trust-new and
sign-new.
If the ConfigMap is forward-dated after this check, the run still halts before
sign-new with the was written by kube-controller-manager at error. Delete the
ConfigMap, restart the workloads in that namespace, and finish by hand as
described below.
Between trust-new and sign-new (--cluster)
Once trust-new has completed on every node, the run:
- Rebuilds its admin client to trust old‖new.
- Waits up to 5 minutes for the
kube-root-ca.crtConfigMap in every namespace to carry the new CA. - Restarts Cilium, the Cilium operator, CoreDNS and local-path-provisioner,
and with
--restart-workloadsyour Deployments, DaemonSets and StatefulSets. It waits up to 10 minutes for them to roll out. A Job or bare Pod is printed asis not restartedand the run continues. - Waits for the old pods to be gone. A rollout is done when the new pods are
ready, and the old pods keep running on the old CA until their grace period
ends. It also waits out the 2-minute margin for a pod whose containers have
not started yet (see below). The wait is up
to the same 10 minutes. A pod that is still on the old CA and is not
terminating or waiting out the margin (a StatefulSet with a partition, an
OnDeleteworkload, or a workload that appeared since the preflight) ends the wait at once and is listed. - Checks, once, that no client is left on the old CA (below).
If a step fails, the run halts before sign-new. Every node already trusts
old‖new and nothing has been promoted, so you can finish the rotation by hand:
- Fix the cause the halt names.
- Run
stratum certs rotate-ca --phase=sign-new --yeson each control plane, then on each worker. Add--skip-workload-checkonly for workloads you will not restart, such as Jobs. Unlike--cluster, this runs the client check on each control plane and does not wait for the node to be Ready. - Confirm every node is Ready,
kubectl get csrshows none pending and the local apiserver/healthzresponds. - Run
stratum certs rotate-ca --phase=finalize --yeson each control plane, then on each worker. A worker'sfinalizereads the new-onlycluster-infobundle that the control plane'sfinalizepublishes, so it refuses until a control plane has finalized. A control plane'sfinalizealso refuses while that node mounts a token signed by the old service-account key. Before the firstfinalize, check every node's tokens with steps 1–4 of Finishing a rotation by hand, which restart the kubelet of a node that has old-key tokens.
To abandon the rotation instead, run stratum certs rotate-ca --rollback --yes
on each control plane, then on each worker. You cannot re-run --cluster: it
refuses to start on a node that is mid-rotation.
This path is only for a halt here, after trust-new has completed on every node.
A halt while trust-new is still running on some node (a control plane or worker
whose trust-new failed) leaves nodes on different trust, and the only way out
is --rollback --yes. A rotation started with --force is the exception: it
cannot be rolled back, and it resumes instead (see
Recovering a lost key).
The wait is bounded as a whole, including each API call it makes. When the old pods outlive it, the halt lists each pod with what is left of its grace period:
1 workload(s) still have pods running on the old CA while they terminate, 10m0s after the rollouts; from sign-new they fail with x509 errors:
Deployment kong/ingress: still terminating on the old CA: ingress-7d9c-x2 (4m12s of its grace period left)
They go when their grace period ends; sign-new can run once they have
Before sign-new (--phase=sign-new)
On a control plane, rotate-ca --phase=sign-new checks every API client before
it promotes the new CA, Stratum's own add-ons included. It refuses while any of
their running containers (regular, init, native sidecar and ephemeral), including
those of pods that are being deleted, started before, or in the same second as,
the write that gave kube-root-ca.crt in its namespace the new CA. For a pod
created before that write, a container that started up to 2 minutes after it
also counts, because the kubelet may not have synced the new ca.crt into the
pod yet. For the same 2 minutes, so does a container of such a pod that has not
started yet (Pending, or waiting out a CrashLoopBackOff), because it can still
start with the old ca.crt:
sign-new refused: 1 workload(s) use in-cluster API credentials and have not restarted since the new CA was published; from sign-new they fail with x509 errors:
Deployment apipoller/apipoller: stratum kubectl -n apipoller rollout restart deployment/apipoller
Restart them before sign-new, or pass --skip-workload-check to sign-new to proceed anyway
Run the listed commands, wait for the rollouts, then run the phase again. A workload whose old pods are still terminating is listed with the grace period they have left: wait for them to go. The new CA stays staged, so nothing needs to be rolled back.
A workload whose pods have not started yet is listed with the time the 2-minute margin ends. It needs no restart: run the phase again after that time.
Deployment apipoller/apipoller: not settled on the new CA: apipoller-5f8c-k2 (not running yet; it can still start on the old CA until 2026-10-09T00:40:00Z)
It also refuses when:
kube-root-ca.crtdoes not carry the new CA yet in some namespace. Finishtrust-newon every node and give kube-controller-manager time to republish it.- It cannot tell when a namespace's
kube-root-ca.crtwas republished, because no kube-controller-manager write is recorded in the ConfigMap'smanagedFields. Pass--skip-workload-checkto proceed. - That write time is more than 5 minutes ahead of this host's clock (
was written by kube-controller-manager at ..., more than 5m0s ahead of this host's clock). Delete the namespace'skube-root-ca.crtso kube-controller-manager recreates it, restart the namespace's API clients, and run the phase again, or pass--skip-workload-checkto proceed.
Under --cluster, the run makes this check itself once, after the restarts and
the wait for the old pods, and before the first sign-new, and halts before
sign-new if it fails. A rollout can
succeed and leave containers on the old CA (a StatefulSet with a
rollingUpdate partition does not restart the ordinals below it, and an
OnDelete workload restarts none of its pods), and a
workload can appear after the preflight. The Jobs and bare Pods that were listed
and not restarted are not counted: they were reported, and halting on them
would stop a run over workloads you were already told about. Each node's sign-new Job then
runs with --skip-workload-check, because the run has done the check, or was
told not to.
A container that restarts in place within about a minute of the republish
can still read the old ca.crt, because the kubelet has not synced the
volume yet. A rollout creates new pods, which mount the current ConfigMap.
certs check lists the 10-year root authorities alongside the leaves. Nothing
renews those automatically; replacing one is rotate-ca, deliberately.
Before finalize: mounted tokens
finalize drops the old service-account signing key. After that, the API
server rejects every token signed with the old key, with 401 Unauthorized.
Before any node finalizes, rotate-ca checks that no pod still mounts such a
token.
The check reads every file in each projected volume under
/var/lib/stratum/kubelet/pods/, because the pod spec chooses the path of the
token inside the volume. For each file that holds a token, it compares the key
ID (kid) in the token's header with the ID of the old key. It reads the files
and does nothing else with them. A file or directory it cannot read fails the
check.
--cluster
After sign-new has completed on every node, the run:
- Scans every node: the node it runs on directly, and every other node with a Job.
- Restarts the kubelet once on each node that mounts an old-key token, then scans that node again. Pods keep running through a kubelet restart, and the restarted kubelet re-issues their tokens within seconds.
- Scans again every minute until no node mounts an old-key token, for up to 60 minutes. A token the restart did not re-issue is refreshed by the kubelet on its own schedule, within about 50 minutes. The 60 minutes include the scans themselves, so a scan still running at the limit ends the check.
- Once every node is clean, waits 2 minutes, so that clients re-read their token files. client-go re-reads its token file once a minute.
- Checks node health again, as it did before the token check: every node is
Ready, the local API server is healthy, and no CSR is pending. The token
check can restart kubelets and take up to an hour, so a node can go NotReady
in the meantime. A failure halts the run before any node finalizes, with the
same
finalize refusedmessage.
The 2-minute wait runs on every --cluster rotation, even when no node had an
old-key token. A run where one node had stale tokens prints:
==> Phase 3/3: finalize preflight (no node mounts a token signed by the old service-account key)
restarting the kubelet: 4 mounted token(s) are signed by the old service-account key
no node mounts a token signed by the old service-account key; waiting 2m0s for clients to re-read their token files
==> Phase 3/3: finalize (drop the old CA)
While a node is not clean, the run prints a waiting for the kubelets to re-issue tokens signed by the old service-account key line each minute that
lists the node and its pods.
If a node still mounts an old-key token after 60 minutes, or a node cannot be scanned, the run halts before any node finalizes. Every node still trusts both CAs and the API server still accepts both keys, so nothing is broken:
finalize refused (cluster left in the safe old‖new state): after 1h0m0s the old service-account key still could not be cleared for finalize: these nodes still mount them: <node>: <namespace>/<pod> (volume <volume>/<path>) (clients holding a token it signed would get 401)
the run halted in sign-new. Run …
The second line goes on to tell you to run stratum certs check on every node,
not to run a single phase on one node, because the phases must finish on every
node in order, and to roll back with stratum certs rotate-ca --rollback --yes
on every node to abandon the rotation. For a rotation started with --force,
which cannot be rolled back, it gives the way to finish it instead.
When a scan was still running at the limit, the list ends with the scan of <node> did not finish. To continue, follow
Finishing a rotation by hand, which finalizes
every node in order, or abandon the rotation with
stratum certs rotate-ca --rollback --yes on every node (not after --force).
The run also warns about each kubernetes.io/service-account-token Secret that
holds an old-key token. These warnings do not stop the run:
WARN: Secret <namespace>/<name> holds a token signed by the old service-account key; it stops working at finalize. Delete the Secret and create it again to get a new one.
Each node's finalize Job runs with the internal --tokens-checked flag,
because the run has already checked every node. A refusal on one node after
another had dropped the old key would leave the rotation half finished.
--phase=finalize
On a control plane, --phase=finalize checks the tokens on that node only. It
does not restart the kubelet and does not wait. If the node mounts an old-key
token, it refuses and lists the pods:
finalize refused: 2 token(s) mounted on this node are signed by the old service-account key, which finalize drops (their clients would get 401): <namespace>/<pod> (volume <volume>/<path>), <namespace>/<pod> (volume <volume>/<path>). Restart this node's kubelet to re-issue them and run finalize again, or pass --skip-token-check
The check covers one node, and workers are not checked at all, so check every
node before you finalize the first one. See
Finishing a rotation by hand. A control plane
that no longer has the old key (/var/lib/stratum/pki/sa.pre-carotate.pub)
skips the check only if its sa-bundle.pub holds a single key or does not
exist, which means it
has already finalized or never rotated. That lets you run finalize again after
a failed restart. If the old key is missing but the bundle still holds two keys,
or the bundle exists but cannot be read or parsed, finalize refuses, because it cannot tell which
tokens the old key signed. Pass --skip-token-check --force only if you accept
the 401 errors that follow. The --skip-token-check
the message suggests also needs --force. See
Skipping the check.
Finishing a rotation by hand
Use this procedure when --cluster halted with finalize refused (cluster left in the safe old‖new state). At that point every node has completed sign-new
and no node has finalized. --phase=finalize checks only its own control plane
and does not wait, so you do the cluster-wide check yourself:
-
On a control plane, compute the ID of the old service-account key. It is the same on every control plane:
OLD_KID=$(sudo openssl pkey -pubin -in /var/lib/stratum/pki/sa.pre-carotate.pub -outform DER \ | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=') echo "$OLD_KID" -
On every node, control planes and workers, scan the mounted tokens. Pass the same
OLD_KIDeverywhere, because workers do not hold the old key:sudo stratum certs rotate-ca --scan-tokens --old-kid "$OLD_KID" --restart-kubelet --yes--restart-kubeletrestarts the node's kubelet once if it finds old-key tokens, then scans again. The command exits0when the node is clean. It exits10when it still finds old-key tokens, and prints onestale-token <pod-uid> <volume>/<path>line for each, for examplestale-token 3f2c…9a1e kube-api-access-x7k2p/token. To name a pod from its UID:kubectl get pods -A -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,UID:.metadata.uid | grep <pod-uid>Delete or restart those pods, or wait for the kubelet to refresh their tokens. To scan again, run the command without
--restart-kubelet. -
Repeat step 2 until every node exits
0in the same pass. -
Wait 2 minutes, so that clients re-read their token files.
-
Check that the cluster is still healthy on the new CA. The run checks the same things before it finalizes:
kubectl get nodes # every node Ready kubectl get csr # no CSR Pending kubectl get --raw /healthz # ok, run on each control planeIf a node is not Ready, a CSR is Pending or
/healthzdoes not answerok, fix that first. It is also what the run halts on when its message names a node that is not Ready or a pending CSR instead of tokens. -
Check that every kubelet serves a certificate the new CA signed. The run checks this before it finalizes, and these checks all pass while a kubelet still serves an old-CA certificate. Like the run, it checks that each certificate is signed by the new CA and names the node's IP. Run this on a control plane, where
sign-newhas madeca.crtthe new CA:for ip in $(kubectl get nodes -o jsonpath='{range .items[*]}{.status.addresses[?(@.type=="InternalIP")].address}{"\n"}{end}'); do case $ip in *:*) addr="[$ip]:10250" ;; *) addr="$ip:10250" ;; esac echo | openssl s_client -connect "$addr" -CAfile /var/lib/stratum/pki/ca.crt -verify_ip "$ip" -verify_return_error >/dev/null 2>&1 && echo "$ip ok" || echo "$ip not verified against the new CA" doneOn a
--ciscluster, skip the control planes: their kubelets renew their serving certificates in their ownfinalize. A node that cannot be reached also prints "not verified"; make its kubelet reachable and check again. For a worker that is not on the new CA, run--phase=sign-newon it again. For a control plane, restart Stratum there (sudo systemctl restart stratum). Finalizing while a kubelet serves an old-CA certificate breakskubectl logsandexecto that node. -
Finalize the control planes one at a time, starting with the one that ran
--cluster. Wait for each to finish before you start the next:sudo stratum certs rotate-ca --phase=finalize --yesEach control plane checks its own tokens again and refuses if it finds one. If that happens, go back to step 2 and do not finalize the other nodes yet.
-
Finalize the workers the same way, one at a time.
-
Run
stratum certs checkon every node to confirm that each one is on the new CA.
After the first control plane finalizes, do not roll back a node that has
finalized: its old CA is gone. Until then, stratum certs rotate-ca --rollback --yes on every node is still the way out, unless the rotation was started with
--force, which cannot be rolled back.
Nobody has run this procedure end to end on a cluster yet. The commands match
what --cluster runs on each node.
What the check does not catch
- Clients that keep the token in memory and never re-read the file. Restart
them after
sign-newhas completed on every control plane and beforefinalize, or they get401once it drops the old key. The 2-minute wait covers clients that re-read the file, as client-go does. - Tokens from
kubernetes.io/service-account-tokenSecrets, and kubeconfigs built from them.--clusterwarns about the Secrets but does not wait for them. See Restart workloads afterrotate-ca.
Skipping the check
| Flag | Applies to | Effect |
|---|---|---|
--skip-token-check | --cluster, --phase=finalize; needs --force | Finalizes without checking the mounted tokens. Pods that still hold an old-key token get 401 once the old key is dropped. |
The flag is for compromise recovery, when you need the old key gone now and
cannot wait for every token to be re-issued. It needs --force, and without it
the command refuses before it changes anything:
stratum certs rotate-ca: --skip-token-check is for compromise recovery and goes with --force
The check also tells you to pass the flag when it cannot read the old key:
cannot tell which tokens the old service-account key signed: <reason> (--skip-token-check finalizes without the check)
Recovering a lost key (--force)
If a control plane has lost the root CA's private key (ca.key under
/var/lib/stratum/pki), the front-proxy CA's key, or the service-account key,
rotate-ca refuses unless you pass --force. With --force the rotation goes
ahead without them and ends on a new root whose key is on every control plane:
sudo stratum certs rotate-ca --cluster --force --yes
Workloads are handled as in any rotation (see
Restart workloads after rotate-ca).
Nothing has to be removed or joined again:
- The old root's certificate (
ca.crt) stays in every trust bundle untilfinalize, as in any rotation, so every node and client, workers included, verifies the new root by it. Only the key is lost; the certificate is public. - From
trust-newuntilsign-newpromotes the new root, the staged root (ca-new.*) signs in place of the lost key: the controller-manager's certificate signer and the operator's webhook certificate. The staged service-account key does the same for a lostsa.key. --clusterpasses--forceto the other control planes'trust-new, so a control plane that lost the same keys goes ahead too.
The keys are the same on every control plane. If only one control plane lost
them, you can instead run a plain rotation (without --force) from a control
plane that still holds them.
A rotation started with --force cannot be rolled back, on any node. A control
plane that lost a key has no old signer to return to, and rolling back the
others would leave them trusting only the old root while it can only go forward.
rotate-ca --rollback refuses on every node that ran its trust-new, and
changes nothing:
pki: refusing to roll back (nothing has been changed): this rotation was started with --force: ...
If the run halts, finish it forward. The halt message says how:
- Before
sign-new(intrust-new, or just after it): fix the cause and runsudo stratum certs rotate-ca --cluster --force --yesagain from the same control plane, adding--restart-workloadsor--skip-workload-checkif the first run had it. It resumes the staged rotation:trust-newruns again on every node with the material already staged, and the run carries on. - In
sign-new: runstratum certs checkon every node, fix the cause, then runsudo stratum certs rotate-ca --phase=sign-new --yeson each control plane and then each worker wheresign-newhas not completed. A control plane whosesign-newpromoted and then stopped, for example on a restart that timed out, finishes where it stopped. Then follow Finishing a rotation by hand.
The old root's certificate is required, even with --force. Without it nothing
could verify the new root, and every component would fail TLS once trust-new
restarts it, so rotate-ca refuses before it changes anything. It refuses a
ca.crt that is not one of the certificates in ca-bundle.crt too, such as a
file copied from another cluster:
certs: refusing to rotate (nothing has been changed): /var/lib/stratum/pki/ca.crt is missing, or is not a certificate in ca-bundle.crt: cannot rotate the CA without the certificate of the root the cluster trusts: ...
Put the certificate back and run the rotation again. Copy ca.crt from another
control plane, or, if this node's ca-bundle.crt holds a single certificate,
copy that file to ca.crt.
The same holds for the other two public halves. A lost sa.pub is rebuilt
from sa.key when that key is still there. Without front-proxy-ca.crt (or
with one that front-proxy-ca-bundle.crt does not carry), or
without both sa.pub and sa.key, trust-new refuses before it changes
anything (cannot rotate the CA without the old front-proxy CA certificate and service-account public key: ...). Copy the file from another control plane, or
copy a single-entry front-proxy-ca-bundle.crt or sa-bundle.pub to it.
On a cluster whose kubelets have their own client CA (see
The kubelet's client trust),
kubelet-client-ca.crt is a fourth public half: without it, or with one that
kubelet-client-ca-bundle.crt does not carry, trust-new refuses the same
way. A lost kubelet-client-ca.key needs --force like the other keys.
A worker that fetches a bundle sharing no CA with its own refuses it in every phase. No rotation produces such a bundle:
refusing to apply the fetched trust bundle: it shares no certificate with this node's on-disk bundle (...), which is a wholesale trust pivot rather than a rotation: every rotation, --force included, keeps the old root in the bundle until finalize, so do not trust this bundle. If the cluster's control planes were rebuilt on a new root, remove this node (`sudo stratum uninstall --yes`) and join it again with a token from `stratum token create` on a control plane
Phases run out of order
Each phase checks that this node completed the phase before it. A refused phase
changes nothing on the node, so you can fix the order and run it again. Every
refusal offers two ways out: run the missing phase on this node, or abandon the
rotation with stratum certs rotate-ca --rollback --yes (not a rotation started
with --force; see Recovering a lost key). Run
stratum certs check to see where the node stands.
| Node | Phase refused | It refuses when | To continue |
|---|---|---|---|
| Control plane | finalize | New CA material is still staged, even a lone ca-new.crt left by an interrupted trust-new, or this node's sign-new never promoted a new CA | Run --phase=sign-new on this node, then finalize. If sign-new ran but its restart failed, run it again once the cause is fixed. If sign-new is refused for want of complete staged material, use --rollback (not after --force) |
| Worker | sign-new | The worker's ca-bundle.crt, kubelet-client-ca-bundle.crt, kubelet.kubeconfig or bootstrap-kubeconfig holds fewer than two distinct CAs, so its trust-new has not run or stopped partway | Run --phase=trust-new on this node again, then sign-new |
| Worker | finalize | The CA that finalize drops still signs the kubelet client certificate. A worker with no CA left to drop is not checked, so a finished finalize can be run again | Run --phase=sign-new on this node, then finalize |
| Worker | sign-new or finalize | The kubelet has no usable client certificate. sign-new renews with the current certificate, so it cannot repair this | Remove the worker with stratum uninstall and join it again with a token from stratum token create |
| Worker | sign-new | The kubelet has no serving certificate, or its renewal CSR under the new CA stays pending or is denied | Check that the operator is running (spec.operator: false turns it off) and why stratum kubectl get csr shows the request unapproved. A SAN the node no longer reports among its addresses stays pending. Approve the CSR by hand if you trust it, then run --phase=sign-new on this node again |
| Worker | finalize | The kubelet serving certificate is missing, unreadable, or still signed by the CA finalize drops | Run --phase=sign-new on this node again, then finalize |
Control plane (--cluster) | finalize | A worker's kubelet serves a certificate the new CA did not sign, or a non-CIS control plane's kubelet does, or cannot be checked at its InternalIP | For a worker, run --phase=sign-new on it again. For a control plane, make its kubelet reachable and restart Stratum there (sudo systemctl restart stratum) so the kubelet serves its new-CA certificate. Then run --cluster again |
Without these checks, finalize on a control plane whose sign-new was
refused would keep the old CA, delete the staged new one, and exit 0. The
cluster stays healthy, but the rotation is silently abandoned. On a worker, it
would leave the kubelet with a client certificate the API server no longer
accepts.
sign-new records that it finished in
/var/lib/stratum/pki/carotate-promoted, after it has promoted the new CA and SA,
reissued the leaves under them and restarted the control plane. If that restart
fails, the node is refused at finalize, because a static pod may still be
serving a certificate the old CA signed. Run sign-new again there once the
cause is fixed: its staged material is already used, so it skips the promotion,
reissues the leaves, restarts the control plane and records it. Rolling back with
stratum certs rotate-ca --rollback --yes also works, except after --force.
A control plane passes finalize only with that file and nothing staged left.
The ca.pre-carotate.crt backup is not proof, because a failed cleanup can leave
one behind after a rollback. sign-new also checks that all the staged files are present before it
promotes anything, and refuses, changing nothing, if one is missing.
A control plane that already finalized also passes, so you can re-run a
finalize whose restart failed. finalize writes carotate-finalized in the
same directory to say so, and removes carotate-promoted. A new trust-new or a
rollback removes both files. finalize, rollback and trust-new fail if they
cannot delete a rotation file. A control plane that never rotated has neither
file, so finalize is refused there too.
A control plane that ran sign-new under an earlier Stratum release has no
carotate-promoted file. If you upgrade it before finalize, finalize is
refused. Run stratum certs rotate-ca --rollback --yes and rotate again.
A control plane that finalized under an earlier release has no
carotate-finalized file, so a finalize that stopped at the restart cannot
be re-run after the upgrade either, though its files are already written and
only the restart is outstanding.
The control-plane refusal reads:
certs: finalize refused: this node's sign-new never promoted a new CA, so finalize would collapse trust to the current CA and discard the rotation — run --phase=sign-new first, or `stratum certs rotate-ca --rollback --yes` to abandon it (see `stratum certs check`)
The stratum-carotate key Secret
While rotate-ca --cluster runs, it stages the AES key that decrypts the shared
CA material (ca.key, sa.key, front-proxy-ca.key) in the
kube-system/stratum-carotate Secret. Anyone with get secrets in kube-system
can read it, so it should exist only while a rotation is in flight.
The run deletes the Secret when it ends, whether it succeeded or failed. It first
waits up to 60 seconds for the API server to pass its health check, so the delete
does not land inside the API server restart. If the API server is still down
after that, the run tries the delete anyway. If the delete fails, the run prints
a WARN with the manual command.
On a control plane, a successful rotate-ca --phase finalize or rotate-ca --rollback also removes a stratum-carotate left behind by an earlier failed
run, but only if it is the key of the rotate-ca --cluster run whose sign-new
phase this node completed. The node records that run when it promotes the new
signer, and keeps the record until the key is known to be gone. A finalize or
rollback that you retry after a failure therefore still removes the key. If the
Secret is already gone, they print nothing.
Two markers tie the key to its run:
stratum.ciq.com/carotate-run: an annotation onstratum-carotatethat holds a random ID for therotate-ca --clusterrun that staged it.migrate-etcd-castages its key without this annotation./var/lib/stratum/pki/carotate-run-id: a file (mode0600) on each control plane that holds the ID of the run whosesign-newthe node completed. Stratum removes it once that run's key is gone. Do not edit it by hand.
migrate-etcd-ca stages the same Secret, and its followers read it while the
migration runs. Finalize and rollback therefore leave the Secret in place and
print a WARN when the kube-system/stratum-etcdca-migration-lock Secret
exists, or when they cannot read it. The warning names the lock's holder and
start time. If that migration run was killed and is not coming back, remove the
lock first, then the key:
kubectl -n kube-system delete secret stratum-etcdca-migration-lock stratum-carotate
Finalize and rollback also print a WARN and leave the Secret in place in three
other cases:
- They cannot read
stratum-carotate. The warning prints the manual delete command. - The Secret is not the key of this node's run. This covers a node with no
carotate-run-idrecord (for example a rollback from thetrust-newphase), a key from a later rotation or frommigrate-etcd-ca, and a rotation driven by manual--phasecommands. Another rotation or migration may still be fetching the key, androtate-ca --clusterpublishes no lock to check. Once no rotation or migration is running, delete the Secret by hand with the command the warning prints. - A newer run replaces the Secret while they are cleaning up. The Secret belongs to that run, and you have nothing to remove.
certs check warns when it finds the Secret while this node reports no CA
rotation in progress:
WARN: kube-system/stratum-carotate exists while no CA rotation is in progress on this node.
It holds the AES key that decrypts the shared cluster CA material (ca.key, sa.key,
front-proxy-ca.key). If no rotate-ca or migrate-etcd-ca run is active, remove it:
kubectl -n kube-system delete secret stratum-carotate
The check only knows this node's rotation phase. Before you delete the Secret by
hand, confirm that no rotate-ca or migrate-etcd-ca run is active on any
control plane. If stratum-etcdca-migration-lock exists, follow the lock-first
order above.
Migrating an existing cluster onto the etcd CA
Clusters built by any released Stratum already issue etcd's certificates from a dedicated authority, separate from the Kubernetes root — see Hardening. They are split from birth and need nothing here.
certs migrate-etcd-ca exists for a cluster built before that split, from a
development checkout predating it. If stratum hardening check reports CIS 2.7
as a FAIL, this is the command it names.
sudo stratum certs migrate-etcd-ca --cluster --yes
It walks three phases across every control plane, completing each one everywhere before starting the next:
| phase | what it does |
|---|---|
trust-both | etcd trusts the etcd CA and the cluster root; every certificate still works |
issue-new | etcd's certificates are reissued under the etcd CA |
drop-root | etcd stops trusting the cluster root — this is the step that closes the hole |
The order is not cosmetic. etcd's trust widens before any certificate moves, and
narrows only after every certificate has moved, so no member ever presents a
certificate its peers do not already accept. drop-root will not start until the
command has confirmed, against each member's running etcd, that every control
plane completed issue-new.
Only control planes are touched. Workers hold no etcd certificate.
If the control planes are not all at the same phase, the command stops and prints
what each one is actually running, rather than picking a phase and hoping. Re-run
with --resume once you have read that table.
A node whose etcd cannot be reached is reported as unknown, and --resume does
not override it — an indeterminate node is not a node that is fine.
If a run stops partway, the cluster is left in the last phase every node
completed, which is a working one. Fix the node and re-run with --resume, or go
back with --rollback:
sudo stratum certs migrate-etcd-ca --cluster --rollback --yes
Rolling back a cluster that has already finished drop-root re-opens the hole,
so it additionally requires --force:
sudo stratum certs migrate-etcd-ca --cluster --rollback --force --yes
That form runs in two passes — it re-adds the cluster root to every control plane's etcd trust first, and only then re-issues the certificates — for the same reason the forward phases are ordered the way they are. Undoing it one node at a time would move a certificate its peers do not yet accept, and that node would leave the etcd cluster immediately.
Migrating an existing cluster onto the kubelet client CA
A cluster created by this release is split from birth: its kubelets trust only
kubelet-client-ca, which signs the API servers' kubelet client certificate
and nothing else (see Hardening). A
cluster created by an earlier release has its kubelets trust the cluster root,
so any certificate the root signs, admin and every node's kubelet client
included, can read logs, run commands and forward ports on any node.
certs migrate-kubelet-client-ca moves such a cluster onto its own CA. Run it
from a control plane once every node runs this release:
sudo stratum certs migrate-kubelet-client-ca --cluster --yes
It runs three phases, each on this control plane, then the other control planes, then the workers, before the next one starts:
| phase | what it does |
|---|---|
trust-both | every kubelet trusts kubelet-client-ca and the cluster root; this control plane creates the CA and the others fetch it, and workers read it from the stratum-kubelet-client-ca ConfigMap |
issue-new | every control plane reissues apiserver-kubelet-client under kubelet-client-ca and restarts its API server |
drop-root | every kubelet stops trusting the cluster root. This is the step that closes the hole |
The kubelets trust the new CA before any certificate moves, and stop trusting
the root only after every API server's certificate has moved, so kubectl logs
and exec keep working throughout. Each phase restarts the kubelets or the API
servers it changes; running pods are not restarted. Before drop-root the
command checks that every kubelet accepts the API servers' new certificate, and
afterwards that every kubelet refuses a certificate the cluster root signed.
sudo stratum hardening check reports the same on any later day: on a control
plane, live.kubelet fails while any kubelet accepts a root-signed
certificate, and on every node kubelet.1 fails while that node's kubelet
bundle holds the cluster root.
Do not join nodes or run certs rotate-ca while the migration runs. A CA
rotation on a node part way through the migration is refused until it finishes
or is rolled back.
If a run stops partway, every kubelet still accepts the API servers. Fix the cause and run the same command again: each node skips the phases it has already completed. To go back to the cluster root instead:
sudo stratum certs migrate-kubelet-client-ca --cluster --rollback --yes
The rollback runs the phases in reverse: every kubelet trusts the cluster root
again, then every control plane reissues apiserver-kubelet-client under it,
then every node drops kubelet-client-ca. On a cluster where any node has
completed drop-root the rollback lets root-signed certificates reach the
kubelets again, so it also needs --force.
A cluster parked part way
A run that halted, or was stopped, leaves its nodes in one of the migration
states. Until drop-root completes on a node, its kubelet still accepts
certificates the cluster root signs. sudo stratum hardening check names each
node's state in the kubelet.1 detail:
| state | meaning |
|---|---|
not-migrated | the kubelet trusts the cluster root alone; the run has not reached this node, or a rollback finished on it |
trust-both | the kubelet trusts kubelet-client-ca and the cluster root |
issue-new | as trust-both, and this control plane's apiserver-kubelet-client is already signed by kubelet-client-ca |
migrated | the kubelet trusts kubelet-client-ca alone; kubelet.1 passes |
While any node is in trust-both or issue-new, certs rotate-ca refuses to
start on it. Finish the migration by running the same --cluster command
again, or go back with --rollback. live.kubelet, on a control plane, shows
which kubelets still accept a root-signed certificate, whatever their files say.
Re-run --cluster rather than --phase by hand. --phase runs one phase on
this node only and is what the cluster run executes on each node; a control
plane other than the one that created kubelet-client-ca can fetch it only
while a --cluster run has it staged.
Talking to the cluster
sudo stratum kubectl <args>
sudo stratum etcdctl <args>
Both are the versions built into this artifact, so they match the cluster, and
on a controller both configure themselves: kubectl against
/var/lib/stratum/kubeconfig/admin.conf, and etcdctl with the etcd mTLS
credentials and the etcd-ca-bundle.crt trust file. The bundle is the right
trust file rather than etcd-ca.crt, because it is what clients verify etcd
against in every trust state, including mid CA rotation.
Your own settings win. Anything already in the environment is left alone, so a variable you set is the one that takes effect:
# talk to a different member, still using this node's client certificate
sudo env ETCDCTL_ENDPOINTS=https://<other-node>:2379 stratum etcdctl member list
On a worker there is no admin.conf, so stratum kubectl sets nothing and
behaves as it always has — pass --kubeconfig, or copy a config to the node.
On a controller, stratum kubectl points at this cluster's admin.conf unless
you say otherwise. If you keep a ~/.kube/config for a different cluster, set
KUBECONFIG explicitly — stratum kubectl will not pick the file up on its own.
admin.conf's rights come from RBAC and can be revoked; the system:masters
super-admin.conf beside it is for recovery. See
admin.conf and super-admin.conf.
Deploying manifests
The operator applies every *.yaml and *.yml file in
/var/lib/stratum/manifests with server-side apply. Drop a file there on a
control plane and it is applied on the next reconcile pass, within about 20
seconds. A file is applied again when its contents change, and every 5 minutes
otherwise.
How a file is treated depends on where it sits:
| Location | Applied | Removed from the cluster when the file goes |
|---|---|---|
/var/lib/stratum/manifests/<file>.yaml | yes | no |
/var/lib/stratum/manifests/<stack>/<file>.yaml | yes | yes |
A first-level subdirectory is a stack. Every object applied from it is
labeled stratum.ciq.com/stack=<stack>. When an object disappears from a
stack's files, the operator deletes it from the cluster. Loose files are only
applied, never pruned.
Because the directory name becomes a label value, a stack name must be a valid
Kubernetes label value: at most 63 characters, made of letters, digits, -,
_ and ., and starting and ending with a letter or digit. Stratum does not
check the name, so every object in a stack with an invalid name fails to
apply.
An object with no metadata.namespace is applied in default, but the
operator records it with no namespace. When you later remove it from the
stack, the delete misses it: the operator logs it as pruned, forgets it,
and leaves it running in default. Set metadata.namespace on every
namespaced object in a stack, default included.
If no *.yaml or *.yml file remains anywhere in
/var/lib/stratum/manifests, the operator skips the pass entirely and
deletes nothing. Objects from a stack whose files were all removed stay
in the cluster until a file is added again.
# the stack's workloads and Services (`get all` skips ConfigMaps, Secrets, RBAC and CRDs)
sudo stratum kubectl get all -A -l stratum.ciq.com/stack=<stack>
# every kind the stack labeled
sudo stratum kubectl api-resources --verbs=list -o name \
| xargs -n1 sudo stratum kubectl get -A --ignore-not-found -o name \
-l stratum.ciq.com/stack=<stack>
Stratum writes its bundled add-ons to this directory as the stacks crds,
node-mark, cilium, coredns and local-path, and rewrites them on
every start. Use other names for your own stacks. An edit to a bundled
stack's file is overwritten.
Only the operator's leader applies the directory, and it reads its own node's
copy. In an HA cluster, put the same files on every control plane, or apply
them with kubectl instead.
The deployer directory is not /var/lib/stratum/config/manifests, which holds
the control-plane static pod manifests that kubelet runs.
The dashboard
Every control plane stages a dashboard stack, and the operator runs it in the
cluster: a single stratum-dashboard Deployment in the stratum-dashboard
namespace, served over HTTPS on NodePort 30443 of every node, and of the VIP
on a cluster that has one. Its image is in
the payload, so it starts with no registry access.
stratum kubectl -n stratum-dashboard get deploy,svc,secret
The dashboard pod itself has no cluster permissions. It forwards each request with the token you signed in with, so you see and change exactly what that token's RBAC allows.
Connect to the dashboard
-
Find the address. The dashboard answers on port
30443of every node, athttps://<node address>:30443. A node's address is itsINTERNAL-IP:stratum kubectl get nodes -o wide stratum kubectl -n stratum-dashboard get svc stratum-dashboardWith a VIP,
https://<vip>:30443works too. If you setpublicURL, use that instead. -
Check the path to the port. Cilium answers NodePort traffic before the host's netfilter rules see it, so firewalld neither needs
30443/tcpopened nor can block it: the dashboard is reachable on every node address that routes to you. To limit who can reach it, filter30443/tcpin a network firewall or security group in front of the nodes, or setnodePort: 0and use a port-forward (below). If you changednodePort, that is the port.If another Service already holds the port, for example a fleet manager exposed on
30443, the operator serves the dashboard on a ClusterIP only and logs why, and the dashboard Service names the holder in an annotation:stratum kubectl -n stratum-dashboard get svc stratum-dashboard \ -o jsonpath='{.metadata.annotations.stratum\.ciq\.com/nodeport-held-by}{"\n"}'Set
spec.dashboard.nodePortto a free port on every control plane and restartstratum upon each, or move the other Service off the port. Once the port is free, the operator puts the NodePort back on its next pass. Until then, use a port-forward. -
Trust the certificate. The certificate is signed by the dashboard's own CA, which your browser does not trust yet. Export the CA:
stratum kubectl -n stratum-dashboard get secret stratum-dashboard-tls \ -o jsonpath='{.data.ca\.crt}' | base64 -d > stratum-ca.crtThen add it to the trust store of the machine that runs the browser:
OS Command macOS sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain stratum-ca.crtRHEL, Rocky, Fedora sudo cp stratum-ca.crt /etc/pki/ca-trust/source/anchors/ && sudo update-ca-trustDebian, Ubuntu sudo cp stratum-ca.crt /usr/local/share/ca-certificates/ && sudo update-ca-certificatesWindows (admin PowerShell) certutil -addstore -f Root stratum-ca.crtFirefox keeps its own store: import the file under Settings → Privacy & Security → Certificates → View Certificates → Authorities.
You are adding a root to that machine, so it matters what the root can vouch for. The dashboard CA is name-constrained: it can only sign for the dashboard's own names,
localhost, thepublicURLhost, and addresses in the subnets of your nodes and VIP (a /24 for IPv4, a /64 for IPv6). It cannot sign another CA, and it is separate from the cluster CA, so arotate-cadoes not change it. Its key is inkube-system/stratum-dashboard-ca: whoever can read that Secret can sign for those names, so treat read access to Secrets inkube-systemaccordingly.For a quick look you can accept the browser's warning instead. Do not make that a habit: the warning is the only thing that would tell you someone is in the middle. To avoid the step entirely, serve your own certificate with
tlsSecretName. -
Get a token. The stack keeps two in Secrets:
Secret ServiceAccount Access stratum-dashboard-admin-tokenstratum-dashboard-admincluster-adminstratum-dashboard-viewer-tokenstratum-dashboard-viewerthe built-in viewrole: read most resources, no Secrets, no changesstratum kubectl -n stratum-dashboard get secret stratum-dashboard-admin-token \ -o jsonpath='{.data.token}' | base64 -d; echoOr use a token for your own ServiceAccount; it can be short-lived:
stratum kubectl create token <serviceaccount> -n <namespace> --duration=8h -
Sign in. Open the address, paste the token into Service account token, and choose Sign in. The session lasts 12 hours, survives a restart of the dashboard pod, and ends early if the cluster stops accepting the token. Sign out from the user menu (Sign out).
The stored tokens do not expire. Anyone who can read Secrets in
stratum-dashboard can take the admin one, so limit that access, or set
tokenSecrets: false and hand out short-lived tokens instead. When the API
server stops accepting a stored token, as after a rotate-ca that replaces the
service-account signing key, the operator replaces the Secret within about five
minutes and the cluster fills in a new token. Get it again and sign in with it.
Without a NodePort
With nodePort: 0, or when the node port is not reachable, forward the Service
to your own machine. This needs only access to the API server:
stratum kubectl -n stratum-dashboard port-forward svc/stratum-dashboard 8443:443
Then open https://localhost:8443. The certificate names localhost and
127.0.0.1, so a trusted dashboard CA works here too.
From a node, without the deployment
stratum dash serves the same dashboard as a process on a control plane, with
no in-cluster objects. It listens on 127.0.0.1:8443 over plain HTTP, the same
trust model as kubectl proxy:
sudo stratum dash
From your own machine, tunnel to it over SSH and open http://127.0.0.1:8443:
ssh -L 8443:127.0.0.1:8443 <user>@<control plane>
If something on the node already holds port 8443 (listen tcp 127.0.0.1:8443: bind: address already in use), pick another with --listen and tunnel to that
one:
sudo stratum dash --listen 127.0.0.1:18443
ssh -L 8443:127.0.0.1:18443 <user>@<control plane>
To serve it on another address, give it a certificate with --tls-cert and
--tls-key; it refuses plain HTTP off loopback. Sign in with a token as above.
When it does not connect
| Symptom | Check |
|---|---|
| The browser times out or the connection is refused | the pod is Ready (stratum kubectl -n stratum-dashboard get pods), a network firewall or security group in front of the node allows 30443, and you used https:// |
| The browser warns about the certificate | the dashboard CA is not trusted on this machine (step 3), or you used an address the certificate does not name: list its names with the command below. A node that just joined is added within a minute |
| The cluster did not accept that token. | the token was cut short when pasted, has expired, or was replaced after a rotate-ca; get it again |
| Cannot reach the dashboard server. | the page loaded but the API calls fail: reload, then check the pod's logs (stratum kubectl -n stratum-dashboard logs deploy/stratum-dashboard) |
| Pages show forbidden | the token's RBAC does not allow it; the viewer token cannot read Secrets or change anything |
openssl s_client -connect <address>:30443 </dev/null | openssl x509 -noout -ext subjectAltName
Certificate
The operator writes the serving certificate into the stratum-dashboard-tls
Secret. It is signed by the dashboard CA, which the operator keeps in
kube-system/stratum-dashboard-ca, out of the dashboard pod's reach. No
Kubernetes component trusts that CA, and its name constraints (step 3 of
Connect to the dashboard) keep it from vouching
for any other site. The certificate names the Service, localhost, every node
address, the VIP and the publicURL host. The operator reissues it when a node
joins and before it ages out, and the dashboard picks up the new one without a
restart. Disabling the dashboard keeps the CA, so browsers that trust it keep
trusting the dashboard when it is turned back on.
The operator replaces the dashboard CA, and logs that it did, when it no longer
covers the dashboard: when publicURL changes, or a node joins from a subnet
the CA does not include. Export and trust the new CA after that. To replace it
yourself, for example if its key may have leaked, delete the Secret; the
operator creates a new CA and reissues the certificate:
stratum kubectl -n kube-system delete secret stratum-dashboard-ca
To serve your own certificate instead, put it in a kubernetes.io/tls Secret in
stratum-dashboard and name it in tlsSecretName.
Configuration
Configure it under spec.dashboard in /etc/stratum/config.yaml, with the same
block on every control plane:
spec:
dashboard:
enabled: true # false removes the stack
nodePort: 30443 # 30000-32767; 0 serves on a ClusterIP only
publicURL: https://dash.example.com # where users reach it, when that is a proxy or a name; https only
tlsSecretName: dash-cert # your certificate instead of the dashboard CA's
enableNodeShell: true # the privileged node shell; default false under cis: true
tokenSecrets: true # keep the admin and viewer sign-in tokens as Secrets
The dashboard needs the operator: with spec.operator: false it is off, and
enabled: true is refused. Setting enabled: false and restarting Stratum on
each control plane removes the stack file, and the operator deletes the
namespace and everything in it. Sessions are signed with the key in the
stratum-dashboard-session Secret.
Logs
| Log | Where |
|---|---|
| Supervised components (containerd, kubelet, the operator) | /var/lib/stratum/log/<component>.log |
| API server audit log, under the CIS profile | /var/log/stratum/audit.log |
| The control-plane static pods | stratum kubectl -n kube-system logs <pod> |