🛸 Hitchhiker's Guide — Phase 04: RBAC & Azure Policy

Read this if: you can click "Add role assignment" in the portal but "why does this service principal get a 403 it shouldn't" still makes you open a ticket. This is the compressed tour the WARMUP derives slowly. Skim it, then read the WARMUP, then build the evaluator.


0. The 30-second mental model

Every Azure write passes two gates in series, both deny-biased:

who?  →  RBAC   (may THIS identity do THIS action at THIS scope?)   → AuthorizationFailed
what? →  Policy (is the resulting RESOURCE allowed to exist?)        → RequestDisallowedByPolicy

RBAC: a role assignment is (principal, role, scope); effective perms = union(Actions) − union(NotActions); deny assignments win; default deny. Policy: an if condition tree over the resource's fields + a then.effect; deny dominates. One sentence to tattoo: authorization is two deny-biased gates over a scope hierarchy, and the control plane and data plane are different permission sets.

1. The four roles you must know cold

RoleActionsThe gotcha
Owner*no DataActions → can't read a blob; can grant access
Contributor* minus Microsoft.Authorization/*manage everything except grant access
Reader*/readsees all, changes nothing
User Access Administrator*/read + Microsoft.Authorization/*manages access, little else

Then the data roles that fix the blob problem: Storage Blob Data Reader / …Data Contributor / …Data Owner, and for secrets Key Vault Secrets User.

2. The numbers and rules to tattoo on your arm

ThingValue
RBAC evaluation orderdeny first → allow → default deny
Effective permsunion(Actions) − union(NotActions) (per plane)
NotActionssubtracts from this role only (not a deny)
Wildcard *matches any chars including / (spans segments)
Scope hierarchyMG → Sub → RG → resource; inherited down
Role assignments per subscription4,000 (hard limit — a real ceiling at scale)
Custom roles per tenant5,000
Management-group tree depth6 levels (excl. root + subscription)
Policy effect precedencedisabled < audit* < append/modify < deployIfNotExists < deny
Request-time effectsdeny, append, modify
Scan-time effectsaudit, auditIfNotExists, deployIfNotExists

3. The az one-liners

# WHO has access here (incl. inherited up the tree)?
az role assignment list --scope <resourceId> --include-inherited -o table

# What can a role actually do?
az role definition list --name "Contributor" --query "[0].permissions" -o json
az role definition list --name "Storage Blob Data Reader" \
  --query "[0].permissions[0].dataActions"            # ← the DataActions

# Grant least privilege: narrow role at the tightest scope (RG, not subscription)
az role assignment create --assignee <objId> --role "Storage Blob Data Reader" \
  --scope <storageAccountId>

# Discover the action strings a provider exposes (the patterns your role matches)
az provider operation show --namespace Microsoft.Storage -o json

# Policy: assign a built-in "deny non-HTTPS storage" at an RG
az policy assignment create --name deny-insecure-storage \
  --policy <builtinDefinitionId> --scope <rgId>

# Policy: what's non-compliant right now?
az policy state summarize --resource-group <rg> -o table

# Custom role (last resort): author exact Actions + tight assignableScopes
az role definition create --role-definition ./my-custom-role.json

4. War story shapes you'll relive

  • "I'm Owner but I can't read the blob!" → control plane vs data plane. Owner has no DataActions. Add Storage Blob Data Reader. The #1 RBAC ticket.
  • "My service principal is Contributor but the deploy says RequestDisallowedByPolicy." → RBAC passed; Policy denied the resource shape (region/tag/SKU/HTTPS). Read the policy's if to find the field. Different gate, different error code.
  • "I removed their assignment and they still have access." → inherited assignment at the RG / subscription / MG. Effective access is the union up the tree; --include-inherited.
  • "An Owner deleted a production resource the stack was protecting!" → they shouldn't have, if there'd been a deny assignment (denySettings). A resource lock isn't enough — Owner can remove a lock; a deny assignment overrides even Owner.
  • "Contributor can't grant my teammate access." → working as designed: Contributor has NotActions: Microsoft.Authorization/*/Write. Grant User Access Administrator (or Owner).
  • "My audit policy isn't blocking anything."audit never blocks; it reports on the scan. If you need to stop the create, the effect is deny.

5. The two-second debugging fork

403 error code?
 ├─ AuthorizationFailed         → RBAC.   Check: assignment exists? right scope?
 │                                          NotActions subtracting it? deny assignment?
 │                                          data-plane action needing a DataAction?
 └─ RequestDisallowedByPolicy   → Policy.  Check: which assignment? which definition's
                                            `if` matched? which field is non-compliant?

6. Vocabulary that signals you've held the pager

  • Role assignment — the (principal, role, scope) triple; the atomic RBAC fact.
  • NotActions — subtraction from a role's own grant, not a deny.
  • DataActions — the data-plane permission set; separate from Actions.
  • Deny assignment — the unconditional override; deny always wins; stack/blueprint-only.
  • Scope inheritance — access flows down MG → Sub → RG → resource.
  • Effective permissions — the union up the tree, minus NotActions, minus deny.
  • Effect — what a policy does on a match; deny dominates.
  • Alias — the name a policy uses for a resource property (vs a raw JSON path).
  • Initiative / policy set — a bundle of policy definitions assigned as one unit.
  • deployIfNotExists — auto-remediation effect; needs a managed identity.

7. Beginner mistakes that mark you in interviews

  1. Granting Owner/Contributor at the subscription when an RG-scoped role would do (blast radius), or inventing a custom role when a built-in already fits.
  2. Thinking Owner implies data access (the blob trap) — or thinking NotActions is a deny.
  3. Forgetting deny is evaluated first and always wins — describing RBAC as "just add up the allows."
  4. Doing scope containment with a raw startswith and getting the rg-vs-rg2 bug.
  5. Confusing the two gates: blaming RBAC for a RequestDisallowedByPolicy (it's Policy).
  6. Using audit where the requirement is "must never be created" (that's deny), or reaching for a resource lock where a deny assignment is required.
  7. Not knowing which effects run at request time vs the compliance scan.

8. How this phase pays off later

  • Phase 05 (Landing Zones) is this, at scale: RBAC + Policy inherited down a management-group tree, an initiative per compliance domain, the ALZ guardrails.
  • Phase 12 (Key Vault / Managed Identity) lives on the control-vs-data-plane split you learned here (Key Vault Secrets User is a DataAction role).
  • Phase 08 (CI/CD) is least-privilege deployment identities — narrow role, tight scope, OIDC federation instead of a secret.
  • Every later phase that touches identity defaults to deny, and you now know exactly why and how that default is enforced.

Now read the WARMUP slowly, then build the evaluator. After that, every "why did this 403?" becomes a function you run in your head.