Phase 04 — RBAC & Azure Policy Evaluation
Difficulty: ⭐⭐⭐☆☆ (the matchers) → ⭐⭐⭐⭐⭐ (the judgment about least privilege, deny, and effect choice) Estimated Time: 1 week (12–18 hours) Prerequisites: Phase 00 (control plane vs data plane, the resource-ID shape and its scope levels), Phase 03 (Entra ID — the token that arrives carries the principal whose access we evaluate here). No live Azure subscription required for the lab.
Why This Phase Exists
Phase 03 ended with a validated JWT: ARM now knows who is calling (the oid/sub
claim) and that the token is genuine. This phase answers the very next question — the one
the entire security posture of an Azure estate turns on: is this caller allowed to do
this, to this resource?
There are two gates, in series, and almost nobody can describe both precisely:
- Azure RBAC — authorization. Given the principal, the action, and the resource's
scope, gather every role assignment that applies, compute the effective permission set,
and let any deny assignment win. This is the gate behind
AuthorizationFailed. - Azure Policy — governance. Even if RBAC says yes, Policy can
denythe write (orappend/modify/auditit) based on the shape of the resource — its SKU, region, encryption setting, tags. This is the gate behindRequestDisallowedByPolicy.
Here is the thing that separates a senior from a principal in an interview: a senior can
assign the Owner role; a principal can explain why that Owner still can't read a
blob (control plane vs data plane), why a Contributor can't grant access to anyone
(NotActions on Microsoft.Authorization/*), why a deny assignment from a Deployment
Stack overrides a subscription-Owner grant, and why a policy with effect: deny
blocks a deployment that RBAC happily authorized. These are not trivia — they are the four
shapes of the support ticket "I have permission but it still fails."
So this phase makes you build both evaluators — the wildcard Actions/NotActions
matcher, scope containment, the deny-wins decision, and the Policy condition-tree
evaluator. Once you've written the algorithm, "effective permissions" stops being a portal
blade you squint at and becomes a function you can run in your head during an incident.
This is the keystone for Phase 05 (landing zones are RBAC + Policy inherited down a
management-group tree at scale) and underpins every later phase that touches identity.
What "Principal-Level" Means Here
A senior assigns roles and writes policies. A principal understands the evaluation machinery well enough to:
- Predict an authorization decision from the assignments without the portal — by
computing
union(Actions) − union(NotActions), checking scope containment, and applying deny-wins, in that order. - Debug the four "I have permission but it fails" tickets: (a) control-plane grant but
data-plane action (
Ownerreading a blob), (b) aNotActionssubtraction (Contributorgranting access), (c) a deny assignment overriding the grant, (d) RBAC allows but Policy denies the resource shape. - Design least privilege: pick the narrowest built-in role that covers the action, reach for a custom role only when no built-in fits, and prefer scope narrowing (assign at the RG, not the subscription) over broader roles.
- Choose the right Policy effect and know when it runs:
deny/append/modifyat the request,audit/auditIfNotExists/deployIfNotExistson the compliance scan — and whydeployIfNotExistsneeds a managed identity. - Reason about blast radius of an assignment: a role at a management group reaches every subscription, RG, and resource beneath it; a deny at an RG stops there.
Concepts
- The RBAC model — a role assignment is the triple
(principal, role definition, scope). RBAC is allow-based (there is no "deny by role"), additive (assignments union together), and inherited down the hierarchymanagement group → subscription → resource group → resource. You grant by adding an assignment and revoke by removing one (or by a deny assignment). - Role-definition internals —
Actions(allowed control-plane operations),NotActions(subtracted from this role's ownActions),DataActions/NotDataActions(the same pair for the data plane), andAssignableScopes(where the role may be assigned). Effective perms =union(Actions) − union(NotActions). Wildcards:*matches any run of characters including/(soMicrosoft.Storage/*/readspans segments). - Control plane vs data plane —
Actionsmanage the resource (create/configure a storage account);DataActionsgovern the data inside it (read a blob). They are separate sets, which is whyOwner(Actions=["*"]) cannot read a blob — it has noDataActions; you addStorage Blob Data Reader. The same split exists for Key Vault, Service Bus, Cosmos DB, and more. - Deny assignments — an explicit block on actions at a scope that overrides role
assignments. Deny always wins, and it is evaluated first. You cannot create deny
assignments with a role; they come from Blueprints, Deployment Stacks
(
denySettings), and managed applications — which is exactly why they're the system's last word for protecting platform-managed resources. - Scope hierarchy & inheritance — a scope is an ARM resource ID (or a prefix). An
assignment at a higher scope applies to all descendants. A resource is "in scope of" an
assignment iff its ID is the scope or sits below it by whole path segments — the
reason
…/resourceGroups/rgcontains…/rg/providers/…but not…/rg2. - Azure Policy — a
policyRuleofif(a condition tree) andthen.effect. Leaf conditions test a field/alias (equals/notEquals/in/exists/like); logical nodes combine them (allOf/anyOf/not). Effects:disabled,audit,auditIfNotExists,append,modify,deployIfNotExists,deny— anddenydominates.deny/append/modifyrun at create/update;audit*anddeployIfNotExistsrun on the compliance scan. - Aliases — Policy can't read raw JSON paths; it reads aliases that map a friendly
name to the resource property at a given API version (e.g.
Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly). The lab models the dotted lookup; the extension wires real aliases.
Labs
Lab 01 — RBAC & Azure Policy Evaluation Engine (flagship, implemented)
| Field | Value |
|---|---|
| Goal | Build both authorization machines: a wildcard action_matches, a RoleDefinition with role_permits (union(Actions) − union(NotActions) and the data-plane pair), segment-aware scope_contains, the full can_perform decision (deny wins, then default-deny over in-scope role assignments), and an evaluate_policy that walks a nested allOf/anyOf/not condition tree with dotted-alias field lookups and returns the effect (plus a compliance_summary roll-up) |
| Concepts | (principal, role, scope) triple; NotActions as subtraction; control-vs-data-plane permission sets; scope-hierarchy inheritance and the rg-vs-rg2 trap; deny-assignment override and evaluation order; Policy condition trees, field operators, effects, and the exists absent-vs-falsey distinction |
| Steps | 1. action_matches (*→.*, escape literals, case-insensitive, anchored); 2. RoleDefinition/role_permits with the data-plane pair; 3. scope_contains (segment-aware prefix); 4. RoleAssignment/DenyAssignment/can_perform (deny-wins → default-deny); 5. evaluate_policy recursive condition tree + dotted lookup; 6. compliance_summary |
| How to Test | pytest test_lab.py -v — 43 tests covering wildcard boundaries (*/read, mid-pattern *, exact, literal-dot, case), NotActions subtraction, the Owner-can't-read-a-blob data-plane split, scope containment incl. the rg/rg2 trap, deny-wins / out-of-scope deny / wrong-principal deny, default-deny + unknown-role, and Policy deny/compliant/nested-allOf-anyOf-not/exists/in/like/dotted-alias + the bad-shape guards |
| Talking Points | "Why can't an Owner read a blob?" / "Walk me through both authorization gates for one PUT." / "Why does a deny assignment beat a subscription-Owner grant?" / "Order the Policy effects and tell me which fire at request time." / "How does a role at a management group reach a single blob?" |
| Resume bullet | Built an offline Azure authorization engine modeling RBAC (wildcard Actions/NotActions matching, control-vs-data-plane permission sets, scope-hierarchy inheritance, deny-assignment override) and an Azure Policy evaluator (nested condition trees, alias lookups, effect resolution, compliance roll-up) |
→ Lab folder: lab-01-rbac-policy-engine/
Integrated-Scenario Suggestions (carried through the whole track)
These compound into Phase 05 (landing zones) and the Phase 15 capstone. Keep them in mind as you build — each plugs the evaluator into a larger governance story:
- Least-privilege CI/CD identity — a deployment service principal that is
Contributoron one RG (not subscriptionOwner), plus the narrowest data-plane roles for the secrets/blobs it touches. The exercise is removing every action it never uses and proving the deploy still works. (→ P08, P12) - Break-glass and deny protection — a Deployment-Stack
denySettingsdeny assignment that blocksdeleteon production resources even forOwner, with a documented break-glass account that is not in the deny scope. (→ P05) - Policy-as-code guardrails — an initiative (policy set) that denies non-HTTPS storage, denies public network access, appends required tags, and audits resources missing a cost-center tag — assigned at a management group so every subscription inherits it. (→ P05)
- Data-plane vs control-plane separation of duties — platform admins get
Ownerto manage a Key Vault but not read its secrets; app identities getKey Vault Secrets User(a data role) but cannot change the vault's firewall. (→ P12) - Effective-access audit — given a principal and a resource, compute the effective
permissions across all inherited assignments and deny assignments — the offline version
of the portal's "Check access" /
az role assignment list --include-inherited. (→ P13)
Guides in This Phase
- HITCHHIKERS-GUIDE.md — the 30-minute orientation; read first
- WARMUP.md — the full primer; read slowly
- BROTHER-TALK.md — the candid version
Key Takeaways
- Authorization is two gates in series. RBAC (
may this identity?) then Policy (is this resource shape allowed?). The four "I have permission but it fails" tickets each map to one specific mechanism — name it and you've solved it. - A role assignment is
(principal, role, scope)— additive, inherited down the hierarchy, and allow-based. You revoke by removing or by a deny assignment, never by a "deny role" (there is none). - Effective perms =
union(Actions) − union(NotActions), and the data plane is a separate set (DataActions).Owner≠Storage Blob Data Reader. - Deny always wins and is evaluated first. A deny assignment from a stack/blueprint
overrides even subscription
Owner— that's its whole purpose. - Pick the narrowest role at the tightest scope. Least privilege is the default; a custom role is a last resort, and broad-scope assignments are a blast-radius decision.
- Choose the Policy effect deliberately —
deny/append/modifyshape the request;audit*/deployIfNotExistsreport and remediate on the scan;denydominates them all.
Deliverables Checklist
-
Lab 01 implemented; all 43 tests pass against
solution.pyand yourlab.py - You can state the RBAC evaluation order (deny first, then allow, default deny) from memory and explain why deny is evaluated first
-
You can explain why
Ownercan't read a blob and which role fixes it -
You can name the four built-in roles (
Owner/Contributor/Reader/User Access Administrator) and the one operationContributorlacks - You can list the seven Policy effects, order them by precedence, and say which run at request time vs the compliance scan
- Given assignments + a resource ID, you can compute the decision on paper