Control-plane guard env applied on both full and fast deploys¶
Motivation¶
The dashboard entry RBAC gate (ENFORCE_DASHBOARD_RBAC) was flipped to true
in Bicep and tests were green, but after a redeploy a no-RBAC user could still
load the full dashboard. Root cause: scripts/dev/quick-deploy.sh — used by
BOTH local fast deploys AND the GitHub Actions deploy.yml path — patches
container images only and never touches env vars. Container App env values
land exclusively through a full azd provision / postprovision Bicep deploy, so
a guard-default change in Bicep silently never reached either fast path. The
live api container had no ENFORCE_DASHBOARD_RBAC env at all (also missing
STRICT_* / ENFORCE_* guards), proving the guard envs had never been
provisioned end-to-end after they were added to Bicep.
User-facing change¶
No UI change. Deployment behaviour change: a control-plane guard/policy env
default set in the repo now applies consistently across all deploy methods —
azd provision, postprovision Bicep redeploy, local quick-deploy.sh, and the
GitHub Actions deploy workflow.
API / IaC diff summary¶
infra/control-plane-env.json(new): single source of truth for the Container App guard/policy toggles, keyed per sidecar (api/worker/beat). Values are strings (OPENAPI_ALLOW_PUBLIC_LB,ENFORCE_OPENAPI_EXEC_RBAC,ENFORCE_DASHBOARD_RBAC,BLAST_GATE_ENABLED,BLAST_JOBS_SHARED_VISIBILITY,STRICT_BLUEGREEN).infra/modules/containerAppControl.bicep: addsvar controlPlaneEnv = loadJsonContent('../control-plane-env.json')and replaces the six api + two worker + two beat literal guard values withcontrolPlaneEnv.<sidecar>.<KEY>references. Inline documentation comments are preserved; only the values now come from the JSON. Compiles cleanly (az bicep build), embeddingENFORCE_DASHBOARD_RBAC: "true".scripts/dev/quick-deploy.sh: reads the same JSON and upserts the per-sidecar guard toggles as--set-env-varson every api/worker/beat PATCH (both the parallelallpath and the single-sidecar path;--no-buildincluded, which is the GHA path).--set-env-varsis an upsert — it only touches the listed keys, leaving image/secret/other env intact. Frontend keeps its existing VITE_* sync; terminal/redis have no guard toggles (no-op). Malformed JSON aborts the deploy; a missing file degrades to image-only.api/tests/test_control_plane_env.py(new): asserts the JSON parses, the expected sidecars/keys exist, all values are strings,ENFORCE_DASHBOARD_RBACstays"true", every JSON key is referenced in the Bicep (controlPlaneEnv.…cross-check so a rename in one place fails CI), and no secret-backed key (e.g.EXEC_TOKEN) leaks into the literal env JSON.
Why this is the right boundary¶
infra/main.jsonis a build artifact —azd provisionrecompilesinfra/main.bicepand postprovision runsaz deployment group create --template-file containerAppControl.bicep, both of which compile the Bicep (and resolveloadJsonContent) at deploy time. No manual recompile needed.- Only literal policy toggles moved to the JSON. Param-derived env (endpoints,
tenant id, secrets via
secretRef) stays inline in Bicep, since quick-deploy cannot and must not re-derive or expose those.
Persona impact (charter 12a)¶
No persona regression. This change does not narrow any RBAC role; it makes an already-decided guard default actually deploy. The dashboard gate itself degrades OPEN if the shared MI cannot read role assignments, so it can never lock out a legitimate operator. Persona Matrix unaffected (no route gate changed, no SSE change).
Validation evidence¶
az bicep build --file infra/modules/containerAppControl.bicep→ compiles; compiled output embeds"ENFORCE_DASHBOARD_RBAC": "true".bash -n scripts/dev/quick-deploy.sh→ syntax OK; helper emits the correct per-sidecar pairs (api 6, worker 2, beat 2, terminal 0).uv run pytest -q api/tests→ 3194 passed, 3 skipped.uv run ruff check api→ clean.- Live remediation (separate from this code change):
az containerapp update --container-name api --set-env-vars ENFORCE_DASHBOARD_RBAC=truealready applied the gate on revisionca-elb-dashboard--0000322; this change makes every future deploy keep it applied automatically.
Hardening discipline (§12a)¶
- In scope: rbac (deploy-path wiring of the
ENFORCE_DASHBOARD_RBACguard) - RBAC change is single-PR safe (no role narrowed) — additive deploy wiring
only; no
roleAssignmentsresource added or removed. - Persona Matrix tests pass for owner / contributor / reader / dev_bypass
(
api/tests/test_persona_matrix.pyunchanged + green). - Reader allowlist unchanged.
- Capability Probe unaffected (no new role introduced).
- RBAC removal preflight N/A — no Bicep
roleAssignmentsdeletion in this change (scripts/dev/check_rbac_removal.pysurface untouched). - New guard ships behind an env toggle —
ENFORCE_DASHBOARD_RBACalready existed (default-OFF originally); this change only makes its repo value deploy consistently.infra/control-plane-env.jsonis the toggle source. - No
Depends(require_caller)added to an SSE event stream. - Change note (this file) summarises persona impact above.