Problem
The node-data-broker is a DaemonSet (charts/topograph/charts/node-data-broker/templates/daemonset.yaml:6) — one pod per node, each mounting the same ServiceAccount token. Its ClusterRole grants nodes: [get,list,update] cluster-wide whenever the subchart is enabled (charts/topograph/charts/node-data-broker/templates/rbac.yaml:9-11).
Kubernetes RBAC cannot scope this to "only my own node": resourceNames is a static list and cannot template per-pod, and the NodeRestriction admission plugin constrains only the system:nodes group (kubelet client certs), not ServiceAccount tokens. So any single compromised node — or any workload that obtains a broker pod's token — can update every Node object in the cluster. Because the schedulers that consume topograph's output (KAI, Kueue TAS, Slinky/Slurm) trust the topology data on nodes, a cluster-wide node-write is the largest blast radius in the chart.
A CEL ValidatingAdmissionPolicy (VAP) is the only mechanism that can constrain what the broker's SA is allowed to change on a Node.
Evidence (re-verified against the repo, not the audit report)
The broker's entire node-write surface is annotations under the topograph.nvidia.com/ prefix — nothing else:
- Write path:
cmd/node-data-broker/main.go:176 Gets its own node (b.nodeName), :181 merges annotations via mergeNodeAnnotations, :183 Updates. mergeNodeAnnotations (cmd/node-data-broker/main.go:270-275) touches only node.Annotations (maps.Copy) — it never writes labels and never deletes keys.
- Keys, per provider dispatched from
getAnnotations (cmd/node-data-broker/main.go:247-268), all defined in pkg/topology/topology.go:43-45:
| Key |
Constant |
Value |
Written by |
| instance |
KeyNodeInstance |
topograph.nvidia.com/instance |
aws imds.go:89, gcp imds.go:89, oci imds.go:122, nebius imds.go:79, dra provider.go:130, lambdai k8s.go:55, infiniband k8s.go:129 |
| region |
KeyNodeRegion |
topograph.nvidia.com/region |
same seven providers (aws imds.go:90 … infiniband k8s.go:130) |
| cluster-id |
KeyGpuClusterID |
topograph.nvidia.com/cluster-id |
infiniband k8s.go:140 only, and only when !useGpuCliqueLabel |
- The
--set/extraArgs "extras" are not written to the node. The only provider that receives them, infiniband, uses them purely for config lookup (pkg/providers/infiniband/k8s.go:84-94,146-148) — they never enter the returned annotations map. So a CEL policy restricting changes to the topograph.nvidia.com/* keyspace does not break any legitimate write, including infiniband with custom gpu-operator-namespace / device-plugin-daemonset overrides.
The audit report's F1 (localdev/rbac-audit-2026-07-09/report.md:82-92) reaches the same conclusion; the enumeration above is independently derived from source. Note: the report's shorthand "relabel/annotate" is imprecise for the broker specifically — the broker writes only annotations, so the tightest correct policy also forbids label changes.
Proposed approach
Ship an opt-in, default-OFF VAP + ValidatingAdmissionPolicyBinding in the node-data-broker subchart, gated on a new node-data-broker.admissionPolicy.enabled value and on the VAP API being served by the cluster. The CEL:
matchConditions restricts the policy to the broker's SA username only, so kubelet and all other identities are untouched (fail-safe: non-broker node updates never evaluate the constraints).
validations require: node labels unchanged; every added/modified annotation key is under topograph.nvidia.com/; no non-topograph annotation removed.
- A best-effort per-node validation: when the cluster issues node-bound SA tokens (
ServiceAccountTokenNodeBinding), the token carries authentication.kubernetes.io/node-name in request.userInfo.extra; the policy then requires the write to target that node. It is a no-op on clusters that don't populate the claim, so it never breaks a legitimate write.
Breaking-change / compatibility note
- Non-breaking, additive, default-OFF. Existing installs are unaffected until an operator sets
admissionPolicy.enabled: true.
- Version floor: VAP is GA (
admissionregistration.k8s.io/v1) in Kubernetes 1.30; the chart's kubeVersion floor is >=1.27.0-0 (charts/topograph/Chart.yaml:4). The template is guarded on .Capabilities.APIVersions.Has "admissionregistration.k8s.io/v1/ValidatingAdmissionPolicy", so on a 1.27–1.29 cluster the resources are silently skipped and helm install does not fail. (Consequence, mirroring the chart's existing httproute.yaml capability note: offline helm template won't render the VAP unless you pass --api-versions "admissionregistration.k8s.io/v1/ValidatingAdmissionPolicy", because Helm only discovers real API versions at install time.)
Residual risk (honest)
- No true per-node pinning without node-bound tokens. All broker pods share one SA identity;
request.userInfo.username is identical across every node's pod. On clusters that do not issue node-bound SA tokens, a compromised broker token can still write topograph.nvidia.com/* annotations to any node — it just can no longer touch labels, other annotations, or (unaffected-identity) anything else. This shrinks the blast radius from "arbitrary write on every node" to "topograph-annotation write on every node," and adds true per-node pinning only where node-bound tokens are in effect.
- Topology annotations remain writable on any node. Because those annotations feed topology generation, a compromised token could still poison a node's
instance/region/cluster-id. Containment is real but partial; it is defense-in-depth, not a complete fix.
- Node
spec/taints are not constrained by this policy version (only labels + annotations). The broker never touches spec, so a spec-equality validation is a candidate hardening, but it is deferred to avoid any risk of rejecting a legitimate annotation-only update until validated live on a kind cluster.
failurePolicy: Fail (fail-closed) means a future apiserver CEL-evaluation error on the broker's own updates would block annotation refresh; the matchCondition and validations are written to not error on well-formed requests, but this is the intended security trade-off.
Acceptance criteria
A staged implementation plan (helm-unittest cases, template edits, docs, CHANGELOG) exists and can follow this issue.
Problem
The
node-data-brokeris a DaemonSet (charts/topograph/charts/node-data-broker/templates/daemonset.yaml:6) — one pod per node, each mounting the same ServiceAccount token. Its ClusterRole grantsnodes: [get,list,update]cluster-wide whenever the subchart is enabled (charts/topograph/charts/node-data-broker/templates/rbac.yaml:9-11).Kubernetes RBAC cannot scope this to "only my own node":
resourceNamesis a static list and cannot template per-pod, and theNodeRestrictionadmission plugin constrains only thesystem:nodesgroup (kubelet client certs), not ServiceAccount tokens. So any single compromised node — or any workload that obtains a broker pod's token — canupdateevery Node object in the cluster. Because the schedulers that consume topograph's output (KAI, Kueue TAS, Slinky/Slurm) trust the topology data on nodes, a cluster-wide node-write is the largest blast radius in the chart.A CEL
ValidatingAdmissionPolicy(VAP) is the only mechanism that can constrain what the broker's SA is allowed to change on a Node.Evidence (re-verified against the repo, not the audit report)
The broker's entire node-write surface is annotations under the
topograph.nvidia.com/prefix — nothing else:cmd/node-data-broker/main.go:176Gets its own node (b.nodeName),:181merges annotations viamergeNodeAnnotations,:183Updates.mergeNodeAnnotations(cmd/node-data-broker/main.go:270-275) touches onlynode.Annotations(maps.Copy) — it never writes labels and never deletes keys.getAnnotations(cmd/node-data-broker/main.go:247-268), all defined inpkg/topology/topology.go:43-45:KeyNodeInstancetopograph.nvidia.com/instanceimds.go:89, gcpimds.go:89, ociimds.go:122, nebiusimds.go:79, draprovider.go:130, lambdaik8s.go:55, infinibandk8s.go:129KeyNodeRegiontopograph.nvidia.com/regionimds.go:90… infinibandk8s.go:130)KeyGpuClusterIDtopograph.nvidia.com/cluster-idk8s.go:140only, and only when!useGpuCliqueLabel--set/extraArgs"extras" are not written to the node. The only provider that receives them, infiniband, uses them purely for config lookup (pkg/providers/infiniband/k8s.go:84-94,146-148) — they never enter the returned annotations map. So a CEL policy restricting changes to thetopograph.nvidia.com/*keyspace does not break any legitimate write, including infiniband with customgpu-operator-namespace/device-plugin-daemonsetoverrides.The audit report's F1 (
localdev/rbac-audit-2026-07-09/report.md:82-92) reaches the same conclusion; the enumeration above is independently derived from source. Note: the report's shorthand "relabel/annotate" is imprecise for the broker specifically — the broker writes only annotations, so the tightest correct policy also forbids label changes.Proposed approach
Ship an opt-in, default-OFF VAP +
ValidatingAdmissionPolicyBindingin the node-data-broker subchart, gated on a newnode-data-broker.admissionPolicy.enabledvalue and on the VAP API being served by the cluster. The CEL:matchConditionsrestricts the policy to the broker's SA username only, so kubelet and all other identities are untouched (fail-safe: non-broker node updates never evaluate the constraints).validationsrequire: node labels unchanged; every added/modified annotation key is undertopograph.nvidia.com/; no non-topograph annotation removed.ServiceAccountTokenNodeBinding), the token carriesauthentication.kubernetes.io/node-nameinrequest.userInfo.extra; the policy then requires the write to target that node. It is a no-op on clusters that don't populate the claim, so it never breaks a legitimate write.Breaking-change / compatibility note
admissionPolicy.enabled: true.admissionregistration.k8s.io/v1) in Kubernetes 1.30; the chart'skubeVersionfloor is>=1.27.0-0(charts/topograph/Chart.yaml:4). The template is guarded on.Capabilities.APIVersions.Has "admissionregistration.k8s.io/v1/ValidatingAdmissionPolicy", so on a 1.27–1.29 cluster the resources are silently skipped andhelm installdoes not fail. (Consequence, mirroring the chart's existinghttproute.yamlcapability note: offlinehelm templatewon't render the VAP unless you pass--api-versions "admissionregistration.k8s.io/v1/ValidatingAdmissionPolicy", because Helm only discovers real API versions at install time.)Residual risk (honest)
request.userInfo.usernameis identical across every node's pod. On clusters that do not issue node-bound SA tokens, a compromised broker token can still writetopograph.nvidia.com/*annotations to any node — it just can no longer touch labels, other annotations, or (unaffected-identity) anything else. This shrinks the blast radius from "arbitrary write on every node" to "topograph-annotation write on every node," and adds true per-node pinning only where node-bound tokens are in effect.instance/region/cluster-id. Containment is real but partial; it is defense-in-depth, not a complete fix.spec/taints are not constrained by this policy version (only labels + annotations). The broker never touches spec, so a spec-equality validation is a candidate hardening, but it is deferred to avoid any risk of rejecting a legitimate annotation-only update until validated live on a kind cluster.failurePolicy: Fail(fail-closed) means a future apiserver CEL-evaluation error on the broker's own updates would block annotation refresh; the matchCondition and validations are written to not error on well-formed requests, but this is the intended security trade-off.Acceptance criteria
charts/topograph/charts/node-data-broker/templates/validatingadmissionpolicy.yaml, default-OFF, gated onadmissionPolicy.enabledand the VAP API capability.node-data-broker.admissionPolicyvalue block withvalues.schema.jsonvalidation;docs/engines/k8s.mdsubsection;CHANGELOG.md[Unreleased]entry.topograph.nvidia.com/keyspace restriction, the label-immutability rule, andvalidationActions: [Deny]; the SA username follows a customserviceAccount.name.make chart-testpasses.A staged implementation plan (helm-unittest cases, template edits, docs, CHANGELOG) exists and can follow this issue.