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.

  1. 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, like spec.authentication (ADR 0094 decision 4) and the Secret references (ADR 0096), which the same webhook already gates with a SubjectAccessReview on a subresource.
  2. 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-secrets and cilium-tlsinterception-secrets (and the operator's counterparts, with RoleBindings) in the namespace given by ingressController.secretsNamespace.name, gatewayAPI.secretsNamespace.name, envoyConfig.secretsNamespace.name, bgpControlPlane.secretsNamespace.name and tls.secretsNamespace.name. The Job may create those Roles by name in any namespace (the roles rule is cluster-wide, escalate and bind are by name). None of the five keys was Ichor-owned, so a value of ichor-system rendered a Role that lets the Cilium ServiceAccounts read the Secrets there. The chart's own defaults are cilium-secrets, except BGP's, which is kube-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 gates spec.authentication and the Secret references, internal/webhook/v1alpha1/override_webhook.go) refuses a create or an update when spec.cni.cilium.values differs from the stored object's, unless the requester holds the verb on that cluster (a SubjectAccessReview for req.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, null and {} 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 TalosCluster spec (ADR 0116, enforced by limitController), so it never changes the values; the automatic patcher writes only the two version fields. Neither is affected.
  • Who holds it. ichor-admin only (config/rbac/ichor_admin_role.yaml and dist/chart/templates/rbac/ichor-user-roles.yaml; dist/chart/user_roles_test.go compares them and checks that viewer and operator lack it). ichor-operator keeps update and patch on talosclusters and 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. SetCiliumValues asks the same question first (mayAll, as for the other subresource verbs) and refuses with a PermissionDenied that names the verb, and GetCiliumValuesResponse.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|edit read can_edit before 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-values creates the TalosCluster with values, so it needs the right too (an adoption was already an admin's act). ichor cni bootstrap renders a manifest locally and writes no TalosCluster.

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.name is kube-system, so a cluster that enables BGP now has its Role in cilium-secrets, and BGP authentication Secrets belong there. This is the one behaviour change of this decision.
  • Takeover. ConfigMapValues reads the five names (and the switches of ingress, Gateway API and BGP) back from the live cilium-config, so OwnedKeyConflicts reports a taken-over Cilium whose BGP secrets are in kube-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), which ponytail: marks.
  • What is not pinned. tls.readSecretsOnlyFromSecretsNamespace=false gives the Cilium agent a cluster-wide get/list/watch on 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/bind pair 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 create as well as name. The name decides where the Role lands; create only 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|diff and 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 owned and in the Cilium page's owned keys.
  • A custom role that today holds update on talosclusters and edits the values loses that until it also holds the new verb.

Risks

  • A user who relied on BGP Secrets in kube-system must move them to cilium-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 override and start; ADR 0108's statement that no built-in role is cluster-admin stands, and ichor-admin is not given escalate.
  • A role binding that grants * on talosclusters/* 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 naming talosclusters/cilium-values; so are ichor 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, and CNIValuesApplied goes True.
  • With the admin: ichor cni values set of bgpControlPlane.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 every cilium-*-secrets Role in cilium-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.readSecretsOnlyFromSecretsNamespace and its deprecated alias tls.secretsBackend (k8s means false, local means true; the chart's readSecretsOnlyFromSecretsNamespace helper lets the flag win, then the alias, then defaults to true) are in OwnedKeys. A user value for either is refused as "set by Ichor", an admin's included. The effective values carry readSecretsOnlyFromSecretsNamespace: true explicitly, 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" into cilium-config only while the flag is true (charts 1.19 and 1.20). ConfigMapValues reads it, and overlayObserved reads 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 to k8s is reported the same way; the chart's unset flag and the alias local equal Ichor's value and are not conflicts. The alias has no ConfigMap key and is read from the release values only.
  • Verified by rendering. BootstrapManifest renders the cilium agent ClusterRole with no rule on secrets, 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 in cilium-secrets. A takeover of a Cilium that read Secrets cluster-wide reports it first.
  • Hardware verification. ichor cni values set tls.readSecretsOnlyFromSecretsNamespace=false and tls.secretsBackend=k8s are refused as set by Ichor. On a freshly installed cluster kubectl get clusterrole cilium -o yaml has no secrets rule, and cilium-config has policy-secrets-only-from-secrets-namespace: "true". Adopting a Cilium whose cilium-config lacks that key lists the conflict.

Search the docs

Search pages and headings