Skip to content

sec(chart): scope auto-upgrade RBAC off cluster-admin (backend#953) - #711

Merged
shujaatTracebloc merged 6 commits into
developfrom
sec/953-scope-auto-upgrade-rbac
Aug 14, 2026
Merged

sec(chart): scope auto-upgrade RBAC off cluster-admin (backend#953)#711
shujaatTracebloc merged 6 commits into
developfrom
sec/953-scope-auto-upgrade-rbac

Conversation

@shujaatTracebloc

@shujaatTracebloc shujaatTracebloc commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

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.

🛑 DO NOT MERGE until live-cluster RBAC verification

No CI test performs a helm upgrade as the auto-upgrade ServiceAccount — scripts/tests/e2e-auto-upgrade.sh upgrades as the cluster-admin kubeconfig user, so it will not catch a missing verb in the scoped role. Before merge, verify on a real cluster that the scoped SA can complete a full chart upgrade:

  1. Install the pre-#953 chart (cluster-admin era), then helm upgrade to this branch's chart (the cutover runs under the old cluster-admin SA — expected to succeed).
  2. Then confirm a subsequent upgrade works under the scoped identity, e.g.:
    helm upgrade <rel> ./client -n <ns> --atomic --reset-then-reuse-values \
      --kube-as-user "system:serviceaccount:<ns>:<rel>-auto-upgrade"
    
    (or impersonate the SA and run kubectl auth can-i --list --as=system:serviceaccount:<ns>:<rel>-auto-upgrade -n <ns> and diff against the resource kinds the chart templates).
  3. Include OpenShift (SCC) in the matrix if feasible.

If the scoped SA 403s on any kind the chart owns, add the missing rule before merging.

What & why

The auto-upgrade CronJob's ServiceAccount was bound to the built-in cluster-admin ClusterRole (templates/auto-upgrade-rbac.yaml), default-on on every client cluster. Anything that compromised this Pod, its token, or the chart repo at repoUrl held 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 is helm upgrade of its own release.

The CronJob runs helm upgrade on 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:

  • namespaced 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.
  • ClusterRole enumerating only the chart's cluster-scoped kindsnamespaces, persistentvolumes, storageclasses, priorityclasses, OpenShift securitycontextconstraints, and clusterroles/clusterrolebindings. escalate/bind on 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)

escalate on 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.

⚠️ Immutable-roleRef rename (why the binding is now -scoped)

A ClusterRoleBinding's roleRef is immutable. Releases before this change already have a binding named exactly <rel>-auto-upgrade whose roleRef is cluster-admin. Reusing that name here (pointing at the new scoped ClusterRole) would make the first helm upgrade onto 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 → pin revision (model-zoo): already mitigated, not by this PR. On develop no zoo file actually passes trust_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 pinning revision= 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.)
  • macOS DMG fail-open: DONE in [Systemic C2] Docker Desktop DMG checksum verification fails open #629. scripts/lib/setup-macos.sh::_verify_docker_dmg fails closed on both a checksum mismatch and an unfetchable/empty checksum.
  • tracebloc-website (jose/CSP/HSTS): delegated to website/frontend.

Tests / verification done here

  • helm lint ./client — 0 failed.
  • helm template … --show-only templates/auto-upgrade-rbac.yaml — renders SA + Role + RoleBinding + ClusterRole + ClusterRoleBinding, no cluster-admin; disabled (autoUpgrade.enabled=false) renders 0 docs; full-chart render OK.
  • helm unittest ./client428 passed. Rewrote the two auto_upgrade_test.yaml assertions that encoded cluster-admin into stronger least-privilege assertions (no coverage removed): namespaced Role is */*/*, bindings target the chart's own roles, ClusterRole carries escalate+bind and no */*/* wildcard, and the binding is -scoped.

Any local helm unittest failures on the schema-negative tests are a helm v4.x artifact (newer JSON-schema error text); CI pins helm v3.15.4 + unittest 0.5.2 where they pass.

🤖 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-admin binding 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-only apiservices for metrics-server lookup), and a new *-scoped ClusterRoleBinding so Helm can create the scoped binding and drop the immutable old cluster-admin binding on the first upgrade. When resource-monitor runs in a separate node-agents namespace, matching Role/RoleBinding are added there so --atomic upgrades do not 403.

Chart 1.9.42, helper/values.yaml trust-boundary comments updated, and helm unittest expectations expanded to assert no cluster-admin, the -scoped binding 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.

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>
@shujaatTracebloc shujaatTracebloc self-assigned this Aug 13, 2026
@shujaatTracebloc
shujaatTracebloc marked this pull request as ready for review August 14, 2026 07:49
shujaatTracebloc and others added 2 commits August 14, 2026 09:51
… (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>
Comment thread client/templates/auto-upgrade-rbac.yaml
…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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread client/templates/auto-upgrade-rbac.yaml
shujaatTracebloc and others added 2 commits August 14, 2026 10:10
…-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 saqlainsyed007 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 OpenShift SecurityContextConstraints — all enumerated. No CRDs, webhooks, or APIService creation exist to be missed. apiservices read-only correctly covers the metrics-server lookup pre-flight, and escalate/bind correctly cover re-applying the chart's own cluster RBAC.
  • Namespaced coverage has no gap. The release-ns Role (*/*/*) plus the conditional node-agents Role (*/*/*). Every namespaced object that renders into a separate node-agents namespace (resource-monitor DS/RBAC, registry secret, image-refresh Role/RoleBinding) is gated on resourceMonitor != 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 -scoped rename sidesteps the immutable roleRef; helm creates the scoped binding and drops the old cluster-admin one in the same pass, run by the still-cluster-admin SA.
  • Tests are meaningful. Ran helm unittest with the CI-pinned plugin 0.5.2: 14/14 pass, and the documentIndex assertions 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.

@shujaatTracebloc
shujaatTracebloc merged commit ee55a9f into develop Aug 14, 2026
22 checks passed
@shujaatTracebloc
shujaatTracebloc deleted the sec/953-scope-auto-upgrade-rbac branch August 14, 2026 10:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants