2026-05-30 — Phase-1 RBAC narrowing for the shared control-plane UAMI (audit P2 #16-20)¶
phase-1 of 2 (see PR-…) — see §12a Rule 1.
Motivation¶
The shared user-assigned managed identity id-elb-dashboard-* historically
held Contributor + User Access Administrator on both
rg-elb-dashboard (platform) and rg-elb-cluster (workload AKS). Audit
items P2 #16-#20 flagged this as overbroad: the API / worker sidecars
only need a small slice of those permissions (manage the AKS cluster,
create / delete the id-elb-openapi UAMI plus its federated credential,
read / write VNet + subnet + NSG for AKS networking). Plain Contributor
also grants Microsoft.*/write on every other resource type in the RG,
including the dashboard's own Storage / Key Vault / Container Apps
control surface — a much larger blast radius than the code path needs.
§12a Rule 1 mandates a two-PR sequence whenever RBAC is narrowed:
- phase-1 (this PR): ADD the narrow roles. Keep the existing
broader
Contributorso every code path continues to work. - phase-2 (separate PR, after a 7-day soak): REMOVE
Contributoronce App Insights shows zeroAuthorizationFailedevents that would have been served byContributor.
This PR is the additive phase-1. No role is removed; the eventual phase-2 PR will reference this PR's number in its description.
User-facing change¶
- None. This is a defence-in-depth RBAC adjustment. Operators see two
more role assignments on the shared UAMI when they run
az role assignment list --assignee <uami-principal-id>. - No UI, no API contract, no Container App template change.
API / IaC diff summary¶
- infra/modules/controlPlaneRoles.bicep:
- Adds three role assignments on the dashboard RG (
rg-elb-dashboard): Managed Identity Contributor(e40ec5ca-96e0-45a2-b4ff-59039f2c2b59)Network Contributor(4d97b98b-1d4f-4787-a291-c67834d212e7)Azure Kubernetes Service Contributor Role(ed7f3fbd-7b88-4dd4-9017-9adb7ce333f8)- The existing
rgContributorresource is kept; itsdescriptionis updated to mark it asPHASE-1 LEGACYso the phase-2 PR's deletion is unambiguous. - New
output …RoleAssignmentId stringentries for the three additions (parity with the existing two outputs). - File-header comment block explains the phase-1 / phase-2 split.
- Adds three role assignments on the dashboard RG (
- infra/modules/workloadClusterRoles.bicep:
- Symmetric change on the workload AKS RG (
rg-elb-cluster): same three roles, samePHASE-1 LEGACYannotation on the existingworkloadRgContributor, same three new outputs.
- Symmetric change on the workload AKS RG (
- No other files touched. No Python, no frontend, no probe, no
postprovision change. The probe in scripts/dev/probe_capabilities.py
will be extended in phase-2 to verify the narrow roles cover every
required surface (currently the existing
Contributormakes the question moot, so adding a probe now would be tautological).
Validation evidence¶
- Bicep compile:
az bicep build --file infra/main.bicep --stdout→ no errors. - Compiled-ARM role-assignment audit:
6 net-new role assignments, 3 per RG, exactly as designed.
Total role-assignment resources in compiled main.json: 20 control-plane-roles/.../contributorRoleId (existing — keep) control-plane-roles/.../userAccessAdministratorRoleId (existing — keep) control-plane-roles/.../managedIdentityContributorRoleId (NEW) control-plane-roles/.../networkContributorRoleId (NEW) control-plane-roles/.../aksContributorRoleId (NEW) workload-cluster-roles/.../contributorRoleId (existing — keep) workload-cluster-roles/.../userAccessAdministratorRoleId (existing — keep) workload-cluster-roles/.../managedIdentityContributorRoleId (NEW) workload-cluster-roles/.../networkContributorRoleId (NEW) workload-cluster-roles/.../aksContributorRoleId (NEW) [+ 10 unchanged: monitoring / sub-roles / workload-rg-creator / acr×3 / storage×2 / kv×2] azd provision --preview(run againstb052302c-4c8d-49a4-aa2f-9d60a7301a80 / rg-elb-dashboard / koreacentral):- Baseline saved at
.tmp/pr8/preview-baseline.txt. - Phase-1 saved at
.tmp/pr8/preview-phase1.txt. - Diff at the top-level resource list: identical. azd preview
intentionally folds nested
Microsoft.Authorization/roleAssignmentsinto the parent deployment and does not enumerate them in its "Resources:" table, so the absence of a Modify line is the expected, correct signal: no Container App template, Storage, Key Vault, VNet, or ACR resource changes — only additive role assignments inside the two RG-scope nested deployments. - Both runs end with
SUCCESS: Generated provisioning preview. - Persona Matrix (§12a Rule 2):
uv run pytest -q api/tests/test_persona_matrix.py→ 41 passed in 3.22s. - Wide sweep (§13):
uv run pytest -q api/tests→ 2152 passed, 3 skipped in 34.42s. - Lint: no Python changed, ruff not re-run.
- Capability Probe (§12a Rule 3): the existing probe (Storage Blob,
Storage Table, ACR, Container Apps, AKS, Key Vault) is unaffected by
phase-1 because
Contributoris still in place. Phase-2 will need a probe extension that exercises the new narrow surfaces (UAMI list, VNet list, AKS write).
Phase-2 hand-off (for the next PR)¶
When the 7-day soak completes and App Insights confirms no
AuthorizationFailed event is attributable to the loss of
Contributor, the phase-2 PR must:
- Delete
rgContributor(controlPlaneRoles.bicep) andworkloadRgContributor(workloadClusterRoles.bicep) — the two resources markedPHASE-1 LEGACY. - Decide whether to also remove
acrContributorForUamiin infra/modules/acr.bicep (it is needed for ACR Build'sscheduleRun/actionand currently has no narrower replacement role — keeping it may be the right call). - Extend scripts/dev/probe_capabilities.py with three new probes that exercise the narrow surfaces directly:
ManagedServiceIdentityClient.user_assigned_identities.list_by_resource_groupNetworkManagementClient.virtual_networks.listContainerServiceClient.managed_clusters.begin_create_or_update(dry-run)- Reference this PR (
#…) in the phase-2 description'sphase-2 of 2 (see PR-N)marker. - Attach an App Insights KQL snapshot of the soak window with zero role-related authorization failures, per §12a Rule 1.
Hardening discipline (§12a):¶
- In scope: rbac
- RBAC change is labelled
phase-1 of 2 (see PR-…)— this PR ADDS the narrow roles only, the broaderContributoris kept in place for the soak window. Phase-2 PR will remove it. - Persona Matrix tests pass for owner / contributor / reader / dev_bypass (41 passed; no auth surface touched in Python)
- Reader allowlist unchanged — no Reader-required route touched
- Capability Probe passes locally — no probe change in phase-1; probe extension scheduled for phase-2 (documented above)
- New guard ships default-OFF — N/A (no
STRICT_*gate; the narrow roles are passive additions, not validation guards per Rule 4 scoping) - No
Depends(require_caller)added to an SSE event stream — no SSE changes - Change note (this file) summarises persona impact: every persona
keeps every existing capability; the shared UAMI gains the ability
to perform AKS / UAMI / Network operations through three narrow
roles instead of relying solely on the broader
Contributor. No persona loses any capability in phase-1.