0131. Changing the Cilium values needs an admin-only verb, and the Cilium Secrets namespaces are pinned
Status: Accepted · Date: 2026-11-05 · Design section: Trust, Webhooks, Cilium · Builds on: 0094, 0096, 0099, 0108, 0129
Decided: A user with only the built-in ichor-operator role could write spec.cni.cilium.values (in the UI, with ichor cni values set, or with kubectl) and so choose what the Cilium Helm Job deploys. The Job's ServiceAccount (ichor-cni) holds escalate and bind on ClusterRoles and Roles by name (ADR 0099 decision 2, ADR 0108), so it is as strong as cluster-admin, and the Cilium chart accepts extraContainers, extraInitContainers, extraHostPathMounts, extraVolumes, extraArgs, image overrides and securityContext: a privileged, host-network pod with a host path on every node is a legal value. That is root on every node, and from there the Talos and Kubernetes PKI and the Secrets in ichor-system: operator became admin-equivalent. This ADR (1) makes changing the values a right of its own, the verb update on talosclusters/cilium-values, held by ichor-admin only and checked by the TalosCluster admission webhook as the requesting user, so the UI, the CLI and kubectl obey it alike, and (2) makes the five *.secretsNamespace.name values of the chart Ichor-owned, pinned to cilium-secrets.
Context
Review of the roles found two ways for a lower role to reach the Job's rights.
- The values are code. ADR 0099 decision 4 validates the values (schema, unknown keys, owned keys, size) but cannot judge them: the chart is a general Helm chart, and any key it renders into a pod spec is available. Banning the dangerous keys one by one would be a list that a chart upgrade or a new alias silently breaks (
extraContainers,extraInitContainers,extraHostPathMounts,extraVolumes,extraArgs,image.override,securityContext.privileged,podSecurityContext,envoy.extra*, ...). The right to write the layer has to be an administrative right, likespec.authentication(ADR 0094 decision 4) and the Secret references (ADR 0096), which the same webhook already gates with a SubjectAccessReview on a subresource. - The namespace of the Secrets Roles. The chart renders Roles named
cilium-ingress-secrets,cilium-gateway-secrets,cilium-envoy-config-secrets,cilium-bgp-control-plane-secretsandcilium-tlsinterception-secrets(and the operator's counterparts, with RoleBindings) in the namespace given byingressController.secretsNamespace.name,gatewayAPI.secretsNamespace.name,envoyConfig.secretsNamespace.name,bgpControlPlane.secretsNamespace.nameandtls.secretsNamespace.name. The Job may create those Roles by name in any namespace (therolesrule is cluster-wide,escalateandbindare by name). None of the five keys was Ichor-owned, so a value ofichor-systemrendered a Role that lets the Cilium ServiceAccounts read the Secrets there. The chart's own defaults arecilium-secrets, except BGP's, which iskube-system.
helm template of the embedded 1.20.4 chart with the features on (ingressController, gatewayAPI, envoyConfig, bgpControlPlane, tls) confirms that these five values, and no other, decide the namespace of a Role or RoleBinding that holds Secret access. The other values that name a namespace (authentication.mutual.spire.install.namespace, clustermesh.mcsapi.corednsAutoConfigure.coredns.namespace, *.serviceMonitor.namespace, dashboards.namespace, k8sServiceLookupNamespace) render Roles and objects the Job has no escalate or bind for, by name, or are covered by the first point.
Decision
1. A subresource verb for the values
The verb is update on talosclusters/cilium-values, the same shape as talosclusters/authentication and talosclusters/secret-references: the constants are ichorv1alpha1.CiliumValuesVerb and ichorv1alpha1.CiliumValuesSubresource.
- Where it is enforced.
OverrideHandler.handleCluster(the admission handler that already gatesspec.authenticationand the Secret references,internal/webhook/v1alpha1/override_webhook.go) refuses a create or an update whenspec.cni.cilium.valuesdiffers from the stored object's, unless the requester holds the verb on that cluster (a SubjectAccessReview forreq.UserInfo, so a hand-written CR obeys it too). On create, "differs" means non-empty. The comparison is on the decoded JSON, so spacing and key order are no change, and an unset,nulland{}layer are the same. An update that leaves the values as they are is never asked, so an operator can keep editing everything else on a cluster whose values an admin set. - Who is exempt. The exemptions of that handler stay as they are (the controller, the namespace and garbage collectors). The controller never writes a
TalosClusterspec (ADR 0116, enforced bylimitController), so it never changes the values; the automatic patcher writes only the two version fields. Neither is affected. - Who holds it.
ichor-adminonly (config/rbac/ichor_admin_role.yamlanddist/chart/templates/rbac/ichor-user-roles.yaml;dist/chart/user_roles_test.gocompares them and checks that viewer and operator lack it).ichor-operatorkeepsupdateandpatchontalosclustersand can still start a Cilium upgrade (the chart is the one Ichor embeds, and the stored values are validated again before the Job, ADR 0129). To delegate the right, bind a ClusterRole with the verb. - The UI and the CLI. The UI backend writes as the impersonated user, so the webhook already covers it.
SetCiliumValuesasks the same question first (mayAll, as for the other subresource verbs) and refuses with aPermissionDeniedthat names the verb, andGetCiliumValuesResponse.can_edit(a new field 7) tells the Cilium page whether the caller may: for an operator the page shows the layer read-only with an explanation instead of an editor.ichor cni values set|editreadcan_editbefore they validate, show the diff or open the editor, so the refusal comes before the confirmation prompt. A write that leaves the layer as it is asks for nothing. - Other writers of the values.
ichor adopt --cilium-valuescreates theTalosClusterwith values, so it needs the right too (an adoption was already an admin's act).ichor cni bootstraprenders a manifest locally and writes noTalosCluster.
2. The secretsNamespace names are Ichor-owned
pkg/cni/cilium adds the five *.secretsNamespace.name keys to OwnedKeys and sets them to cilium-secrets in Ichor's values (secretsNamespaceOf). A user value for any of them, whatever it is (even cilium-secrets, an empty string or null), is refused as "set by Ichor and cannot be overridden" by ValidateValues, in the webhook, the backend, the CLI and the controller's check before every Job. The owned layer applies last, so the rendered Roles are always in cilium-secrets, which the chart creates (tls.secretsNamespace.create is true by default).
- BGP. The chart's default for
bgpControlPlane.secretsNamespace.nameiskube-system, so a cluster that enables BGP now has its Role incilium-secrets, and BGP authentication Secrets belong there. This is the one behaviour change of this decision. - Takeover.
ConfigMapValuesreads the five names (and the switches of ingress, Gateway API and BGP) back from the livecilium-config, soOwnedKeyConflictsreports a taken-over Cilium whose BGP secrets are inkube-system. A feature that is off is not a conflict (its chart default is only a default). The Envoy-config switch is not read back from an inline manifest (its ConfigMap key is also set by ingress and Gateway API), whichponytail:marks. - What is not pinned.
tls.readSecretsOnlyFromSecretsNamespace=falsegives the Cilium agent a cluster-wideget/list/watchon Secrets, and the chart's operator ClusterRole holds the same when ingress, Gateway API, BGP or secret sync is on. Both are Cilium's own design for those features and sit behind decision 1, so only an admin can set them. They are not owned.
Alternatives rejected
- A deny list of dangerous chart keys. It must be complete for every chart version Ichor embeds and every alias, and an omission is root on the nodes. The admin-only verb needs no list and covers the keys no one has thought of.
- A validating admission policy on the rendered Job's pods. It would judge what the Job creates rather than who asked, but Ichor does not own the admission of the managed cluster (and it would be the user's cluster configuration, not Ichor's), and the Job's pods are the chart's own.
- Giving the Job a narrower ServiceAccount. The
escalate/bindpair is the reason the Job exists as a separate identity (ADR 0099 decision 2); the chart's ClusterRoles need it. Narrowing it is not possible for the chart's cluster-scoped objects. - Checking only in the UI backend. It would leave
kubectl, which is the rule ADR 0129 exists to enforce. - Pinning
createas well asname. The name decides where the Role lands;createonly decides whether the namespace object is rendered, and a user who turns it off breaks the install, not Ichor's security.
Consequences
- An operator can no longer change Cilium's values by any route; the Cilium page shows them read-only,
ichor cni values get|diffand the validation calls still work. A cluster whose values an admin set stays editable by an operator in every other field. - The five names are visible, with the reason, in
ichor cni values get --layer ownedand in the Cilium page's owned keys. - A custom role that today holds
updateontalosclustersand edits the values loses that until it also holds the new verb.
Risks
- A user who relied on BGP Secrets in
kube-systemmust move them tocilium-secrets; a takeover reports the difference before it changes anything. - The right is as strong as the Job. An admin who holds it is trusted with root on the nodes, as an admin already is through
overrideandstart; ADR 0108's statement that no built-in role is cluster-admin stands, andichor-adminis not givenescalate. - A role binding that grants
*ontalosclusters/*grants the verb too; the chart's invariant tests keep the built-in roles free of wildcards.
Hardware verification
- With a user bound only to
ichor-operator:kubectl patch talosclusters/<name> --type merge -p '{"spec":{"cni":{"cilium":{"values":{"debug":{"enabled":true}}}}}}'is refused with the message namingtalosclusters/cilium-values; so areichor cni values set|edit(before the prompt) and the Cilium page's editor (read-only, with the notice). The same edit of a label is admitted. - With a user bound to
ichor-admin: the same patch is admitted, the controller applies it, andCNIValuesAppliedgoes True. - With the admin:
ichor cni values setofbgpControlPlane.secretsNamespace.name: ichor-system(and of the other four keys) is refused as set by Ichor, and a rendered release with BGP, ingress, Gateway API and Envoy config on has everycilium-*-secretsRole incilium-secrets.
Amendment (2026-11-06): the agent's cluster-wide Secret read is pinned off
"What is not pinned" above left tls.readSecretsOnlyFromSecretsNamespace=false to the admin-only verb. That is not enough for the agent: the Cilium agent is a privileged pod on every node, and with the flag false the chart's agent ClusterRole grants it get, list and watch on Secrets of every namespace, so one compromised node exposes ichor-system (the secrets bundle with the CA keys). Ichor hardens itself strictly, and nothing Ichor installs needs that read, so the agent's read is now Ichor's, like the five namespaces.
- Owned keys.
tls.readSecretsOnlyFromSecretsNamespaceand its deprecated aliastls.secretsBackend(k8smeans false,localmeans true; the chart'sreadSecretsOnlyFromSecretsNamespacehelper lets the flag win, then the alias, then defaults to true) are inOwnedKeys. A user value for either is refused as "set by Ichor", an admin's included. The effective values carryreadSecretsOnlyFromSecretsNamespace: trueexplicitly, so a chart that changes its default cannot widen the read. The alias is left unset, which the flag overrides. - Takeover. The chart writes
policy-secrets-only-from-secrets-namespace: "true"intocilium-configonly while the flag is true (charts 1.19 and 1.20).ConfigMapValuesreads it, andoverlayObservedreads its absence from an existing ConfigMap as false, so a running Cilium with the cluster-wide read is an owned-key conflict (informational, like the namespaces), reported before the adoption narrows it. A release whose values set the flag to false or the alias tok8sis reported the same way; the chart's unset flag and the aliaslocalequal Ichor's value and are not conflicts. The alias has no ConfigMap key and is read from the release values only. - Verified by rendering.
BootstrapManifestrenders theciliumagent ClusterRole with no rule onsecrets, with and without ingress, Gateway API, Envoy config and BGP on. - Still not pinned. The operator's cluster-wide Secret read with ingress, Gateway API, BGP or secret sync on is unchanged: it is the operator Deployment, not a privileged pod on every node, and stays behind the admin-only verb.
- Consequence. A TLS-aware
CiliumNetworkPolicy(terminatingTLS,originatingTLS) must reference Secrets incilium-secrets. A takeover of a Cilium that read Secrets cluster-wide reports it first. - Hardware verification.
ichor cni values set tls.readSecretsOnlyFromSecretsNamespace=falseandtls.secretsBackend=k8sare refused as set by Ichor. On a freshly installed clusterkubectl get clusterrole cilium -o yamlhas nosecretsrule, andcilium-confighaspolicy-secrets-only-from-secrets-namespace: "true". Adopting a Cilium whosecilium-configlacks that key lists the conflict.