0011. Reconcile the machine-config backup credential onto the ServiceAccount CRD¶
Status: Accepted, 2026-08-16
Context¶
ADR 0009 and ADR 0010 each solved the same underlying problem, a pod needing an
os:admin Talos API credential, with two different mechanisms:
cluster-backup'smachineconfig-backupCronJob (ADR 0009) used a static certificate minted once withtalosctl config new --roles os:admin, SOPS-encrypted and committed. Chosen specifically to avoid atalos/talconfig.yamledit, which was out of scope for that change. The certificate was never actually minted, so the CronJob shippedsuspend: trueand did nothing.tuppr(ADR 0010) used thetalos.dev/v1alpha1ServiceAccountCRD instead: a pod requests a role via a Kubernetes-native custom resource, and Talos mints the matching Secret itself oncemachine.features.kubernetesTalosAPIAccessgrants the requesting namespace and role. ADR 0009 flagged this as "a reasonable thing to revisit the next timetalos/changes for its own, unrelated reason." ADR 0010 is that reason: the machine config edit tuppr needed anyway removes the only reason ADR 0009 gave for not using it.
Rule 11 (one way to do each thing) argues against carrying two credential mechanisms for what is the same privilege level solving the same kind of problem.
Decision¶
Move the machineconfig-backup CronJob onto the ServiceAccount CRD, matching tuppr:
talos/talconfig.yaml'skubernetesTalosAPIAccesspatch gains a second entry inallowedKubernetesNamespaces,system-backup, alongsidesystem-upgrade.allowedRolesstays[os:admin], already the role both consumers need.kubernetes/apps/system-backup/cluster-backup/app/talos-serviceaccount.yamldeclares atalos.dev/v1alpha1 ServiceAccountnamedmachineconfig-backup-talosconfigrequestingos:admin, the same pattern as tuppr's chart-managed one.- The CronJob mounts the resulting
machineconfig-backup-talosconfigSecret instead of the oldtalos-machineconfig-backupone. Talos mints that Secret with aconfigkey, nottalosconfig, so the volume remaps it withitemsto the filenametalosctlexpects. talos-machineconfig-backup.PLACEHOLDER.yamlis deleted. There is no static credential left to mint, encrypt, or rotate for this job.spec.suspendflips tofalse. The only reason it wastruewas the missing credential; the credential now provisions itself the moment this PR merges and the machine config grant is applied, so there is no remaining reason to keep it off.
The etcd-snapshot job is untouched, on purpose. It uses os:etcd:backup, the
narrowest role Talos has, exactly one RPC and nothing else. Moving it onto the shared
kubernetesTalosAPIAccess grant would work mechanically, but it would also mean the
system-backup namespace's standing grant has to list a second role, widening what any
future pod in that namespace could request. The machine-config job already needs
os:admin, so consolidating onto it costs nothing that wasn't already true; giving the
etcd job the same blast radius for no reason would be a regression, not a reconciliation.
It keeps its own static cert per ADR 0009. Minted and unsuspended 2026-08-16, once
this PR merges the reasoning above no longer required both jobs to move together.
Consequences¶
Good: one mechanism for os:admin-level Talos API access from a pod, not two. No SOPS
file to encrypt, no one-time talosctl config new step, no yearly rotation to remember,
for this credential specifically, since Talos mints and rotates it itself. The
machineconfig-backup job actually runs now instead of shipping as an inert manifest.
Bad: allowedKubernetesNamespaces naming a second namespace means the standing
os:admin grant is no longer scoped to a single controller in a single namespace, the
property ADR 0010 leaned on to justify the risk (rule 11 in the small: "one namespace,
one controller, one credential"). Any pod that lands in system-upgrade or
system-backup and can request the talos.dev/v1alpha1 ServiceAccount CRD with role
os:admin gets full node control. Both namespaces are still single-purpose today (tuppr
only; the two backup CronJobs only), so the practical blast radius is unchanged, but the
grant itself is now a list, not a singleton, and that is worth noticing the next time
something else wants a namespace added to it.