sec(chart): scope auto-upgrade RBAC off cluster-admin (backend#953) - #711
Conversation
The auto-upgrade CronJob's ServiceAccount was bound to the built-in cluster-admin ClusterRole, giving a standing cluster-wide grant to a component whose only job is `helm upgrade` of its own release. Replace it with least-privilege RBAC: - a namespaced Role (*/*/*) confined to the release namespace, covering every namespaced kind the chart templates plus helm's readiness watches and release-state Secrets; - a ClusterRole enumerating only the chart's cluster-scoped kinds (Namespace, PersistentVolume, StorageClass, PriorityClass, ClusterRole/Binding, OpenShift SCC), with escalate/bind on the RBAC kinds so upgrades can re-apply the chart's other ClusterRoles. The new ClusterRoleBinding is renamed `-scoped` because roleRef is immutable: reusing the pre-existing cluster-admin binding's name would make the first upgrade fail on every deployed client. A distinct name lets helm create-new + delete-old; the cutover upgrade still runs under the previous cluster-admin SA. Default stays ON (#69). Updates the now-stale cluster-admin comments in values.yaml and _helpers.tpl, rewrites the auto_upgrade unittest assertions to lock in the least-privilege contract, and bumps Chart.yaml version/appVersion (chart-version-guard). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… (backend#953) develop's Chart.yaml advanced to 1.9.40 — the version this branch bumped to — so chart-version-guard's version-check failed. Bump version+appVersion to 1.9.42 in lockstep (staggered above the sibling client#564 PR's 1.9.41) and merge origin/develop to stay current. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…backend#953) Bugbot (High): the scoped auto-upgrade Role was confined to the release namespace, but the chart also templates resource-monitor + image-refresh objects into the separate tracebloc-node-agents namespace (gated on resourceMonitor!=false). After the cluster-admin cutover a 'helm upgrade --atomic --wait' tick would 403 on those and auto-upgrade could not land. Add a namespaced Role+RoleBinding for the SA in the node-agents namespace, rendered only when it is a separate namespace and resource-monitor is enabled. Update the least-privilege unittest doc-selectors (a second Role now exists) and add node-agents-RBAC coverage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 347a570. Configure here.
…-upgrade-rbac # Conflicts: # client/Chart.yaml
…-flight (backend#953) Bugbot (High): resource-monitor-daemonset.yaml pre-flights metrics-server via a helm `lookup` of the v1beta1.metrics.k8s.io APIService. After the cluster-admin cutover the scoped SA runs that lookup during `helm upgrade`; a 403 makes lookup return empty, the template's `fail` misreads it as 'metrics-server missing', and every later auto-upgrade tick aborts. Add read-only get/list/watch on apiregistration.k8s.io/apiservices to the scoped ClusterRole, with a unittest guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
saqlainsyed007
left a comment
There was a problem hiding this comment.
Code review — correctness. Clean, approving. ✅
I reviewed the RBAC scope-down for correctness and could not find a defect. What I checked:
- ClusterRole enumeration is complete. The only cluster-scoped kinds the chart templates are
Namespace,PersistentVolume,StorageClass,PriorityClass,ClusterRole/ClusterRoleBinding, and the OpenShiftSecurityContextConstraints— all enumerated. No CRDs, webhooks, or APIService creation exist to be missed.apiservicesread-only correctly covers the metrics-serverlookuppre-flight, andescalate/bindcorrectly cover re-applying the chart's own cluster RBAC. - Namespaced coverage has no gap. The release-ns
Role (*/*/*)plus the conditional node-agentsRole (*/*/*). Every namespaced object that renders into a separate node-agents namespace (resource-monitor DS/RBAC, registry secret, image-refresh Role/RoleBinding) is gated onresourceMonitor != false— the exact gate on the new node-agents Role — so there's no configuration where an object lands there without the SA holding rights. - Cutover is sound. The
-scopedrename sidesteps the immutableroleRef; helm creates the scoped binding and drops the oldcluster-adminone in the same pass, run by the still-cluster-admin SA. - Tests are meaningful. Ran
helm unittestwith the CI-pinned plugin0.5.2: 14/14 pass, and thedocumentIndexassertions align with helm-unittest's source ordering (verified against a real render), so the new assertions actually exercise the right documents.
Not blocking, but flagging: this approval is on code correctness only. The PR's own 🛑 DO NOT MERGE until live-cluster RBAC verification banner still stands — no CI test runs helm upgrade as the scoped SA, so a missing verb would only surface on a live cluster. Please complete that verification (including the OpenShift/SCC matrix) before the release train picks this up.

Part of the Sec-II supply-chain hardening tracked in tracebloc/backend#953. Cross-repo issue, so no
Closes— backend#953 will be closed manually once all sub-items land.What & why
The auto-upgrade CronJob's ServiceAccount was bound to the built-in
cluster-adminClusterRole (templates/auto-upgrade-rbac.yaml), default-on on every client cluster. Anything that compromised this Pod, its token, or the chart repo atrepoUrlheld a standing grant to read every Secret in every namespace, exec into any pod, and mutate any workload cluster-wide — a large blast radius for a component whose only job ishelm upgradeof its own release.The CronJob runs
helm upgradeon the whole chart, so its SA must be able to CRUD every kind the chart templates (and watch workloads for--atomic --wait). Least-privilege split:Role(*/*/*) confined to the release namespace — covers every namespaced kind the chart owns, helm's readiness watches, and its release-state Secrets; and, being*, does not break when a future chart version adds a new namespaced kind. The cross-namespace reach that made cluster-admin dangerous is gone.ClusterRoleenumerating only the chart's cluster-scoped kinds —namespaces,persistentvolumes,storageclasses,priorityclasses, OpenShiftsecuritycontextconstraints, andclusterroles/clusterrolebindings.escalate/bindon the RBAC kinds are mandatory: Kubernetes' privilege-escalation prevention would otherwise refuse to let this (non-cluster-admin) SA re-apply the chart's other ClusterRoles/bindings (rbac.yaml,resource-monitor-rbac.yaml, …) during an upgrade.Default stays ON (#69): defaulting off would recreate the exact "customers freeze on their installed version and never receive security fixes" bug, so a scoped role — not an opt-out — is the fix.
Residual (documented, intentional trade)
escalateon clusterroles is powerful, but it is an active, auditable step rather than a standing broad grant, and the worst cluster-admin primitives (cluster-wide secret read, pod exec, node access) are simply absent. Adding a new cluster-scoped kind in a future chart version means extending this ClusterRole in the same release (helm applies ClusterRole before most cluster kinds); a new cluster kind that sorts before ClusterRole in helm's install order (e.g. StorageClass) would need a one-time cluster-admin-assisted upgrade.roleRefrename (why the binding is now-scoped)A
ClusterRoleBinding'sroleRefis immutable. Releases before this change already have a binding named exactly<rel>-auto-upgradewhoseroleRefiscluster-admin. Reusing that name here (pointing at the new scoped ClusterRole) would make the firsthelm upgradeonto this version fail with "cannot change roleRef" on every already-deployed client. So the new binding is named<rel>-auto-upgrade-scoped: the old name drops out of the rendered manifest set, so helm creates the scoped binding and deletes the old cluster-admin one in the same upgrade. That cutover upgrade still runs as the previous cluster-admin-bound SA, so it has the rights to do both; every later tick runs scoped.The other #953 sub-items (context — no code here)
trust_remote_code→ pinrevision(model-zoo): already mitigated, not by this PR. Ondevelopno zoo file actually passestrust_remote_code=True, and the backend blocks any*.from_pretrained(...)at model upload (backend#1495 / RFC-BACKEND-1501; bandit plugin +system_tests/test_model_upload_hf_blocked.py). The hub-fetch vector never reaches a training node, so pinningrevision=on code the platform already rejects would be theater. (Follow-up filed separately: the 4 hub-fetch zoo models carry now-false "passes the platform security check" comments.)scripts/lib/setup-macos.sh::_verify_docker_dmgfails closed on both a checksum mismatch and an unfetchable/empty checksum.Tests / verification done here
helm lint ./client— 0 failed.helm template … --show-only templates/auto-upgrade-rbac.yaml— renders SA + Role + RoleBinding + ClusterRole + ClusterRoleBinding, nocluster-admin; disabled (autoUpgrade.enabled=false) renders 0 docs; full-chart render OK.helm unittest ./client— 428 passed. Rewrote the twoauto_upgrade_test.yamlassertions that encoded cluster-admin into stronger least-privilege assertions (no coverage removed): namespaced Role is*/*/*, bindings target the chart's own roles, ClusterRole carriesescalate+bindand no*/*/*wildcard, and the binding is-scoped.🤖 Generated with Claude Code
Note
High Risk
Changes default-on auto-upgrade identity and cluster RBAC on every client; a missing verb or cutover mistake can break fleet chart upgrades until live-cluster verification passes.
Overview
Replaces the auto-upgrade CronJob ServiceAccount’s
cluster-adminbinding with least-privilege RBAC so a compromised upgrade pod or chart repo no longer has standing cluster-wide power (backend#953).The SA now gets a full namespaced Role in the release namespace, a chart-specific ClusterRole limited to cluster kinds the chart actually manages (namespaces, PVs, storage classes, priority classes, OpenShift SCCs, cluster RBAC with
escalate/bind, and read-onlyapiservicesfor metrics-serverlookup), and a new*-scopedClusterRoleBinding so Helm can create the scoped binding and drop the immutable oldcluster-adminbinding on the first upgrade. When resource-monitor runs in a separate node-agents namespace, matching Role/RoleBinding are added there so--atomicupgrades do not 403.Chart 1.9.42, helper/
values.yamltrust-boundary comments updated, and helm unittest expectations expanded to assert nocluster-admin, the-scopedbinding name, and conditional node-agents RBAC.Reviewed by Cursor Bugbot for commit 807a8d8. Bugbot is set up for automated code reviews on this repo. Configure here.