You run one command — az storage account create, a portal Save, a terraform apply — and Azure answers with a wall of red:
(AuthorizationFailed) The client 'a1b2c3d4-...' with object id 'a1b2c3d4-...'
does not have authorization to perform action 'Microsoft.Storage/storageAccounts/write'
over scope '/subscriptions/.../resourceGroups/rg-prod' or the scope is invalid.
If access was recently granted, please refresh your credentials.
This is the most common access error in Azure, and it is maddening precisely because it is honest. The message names the principal (who), the action (what), and the scope (where) — and that triple is the whole diagnosis. Azure RBAC (Role-Based Access Control) is the authorization system that decides, for every control-plane call to Azure Resource Manager (ARM), whether a given identity may perform a given action on a given resource. When it says AuthorizationFailed, it is not broken — it evaluated your effective permissions and found no role granting that action at that scope, or found a deny that overrides everything you were granted. The skill is reading the message correctly, then reading your effective permissions to confirm which of the two it is.
This is the playbook for that skill. We treat AuthorizationFailed not as one error but as a family of root causes — no assignment, wrong scope, propagation lag, a deny assignment from a managed app or Blueprint, a NotActions subtraction, a stale token that predates your new role, ABAC condition failure, a resource lock masquerading as a permissions error, and the control-plane-versus-data-plane trap where you have the role but the call needs a different one. For each you’ll learn the exact az role assignment list, az role definition, az provider operation and Check access path that confirms it, and the precise grant that fixes it. Because RBAC is layered (management group → subscription → resource group → resource), most of the work is figuring out where a permission is or isn’t — a reading exercise the tools make mechanical once you know which one to open.
By the end you will stop guessing and re-running with --debug. When the error fires you’ll parse the principal, action and scope in five seconds, decide whether you’re missing a grant or hitting a deny, run the one command that proves it, and assign the smallest role at the right scope — without handing out Owner and without waiting on a ticket. If you’re new to roles and scopes, read Azure RBAC Fundamentals: Roles, Scopes and Assignments Without the Confusion first; this article assumes that base and goes straight to the failure modes.
What problem this solves
RBAC is invisible until it says no. You build, you deploy, the assignment someone made months ago quietly covers you — until you touch a resource at a scope nobody granted, or a guardrail you didn’t know existed denies the write. Then the bare AuthorizationFailed string is all you get, and because it deliberately reveals nothing about what role would have worked, an engineer who can’t read it burns an hour. The information you need is real and queryable — Azure will tell you exactly which assignments the principal has and exactly which action the call required — but it lives across az role assignment list, the Check access / Effective access blade, az provider operation show, and the Activity log, and if you don’t know which one maps to which cause you click in circles.
What breaks without this knowledge is predictable and expensive. The on-call engineer escalates to a subscription Owner who “fixes” it by granting Owner at the subscription (now an over-privileged identity sits in production forever, an audit finding waiting to happen). A pipeline’s service principal fails a terraform apply and the team disables a policy, or worse, runs the deploy as a human admin, defeating the entire point of automation. A Contributor swears they have access yet still can’t grant a role to a managed identity — because granting roles needs Microsoft.Authorization/roleAssignments/write, which Contributor does not have, and nobody reads the role definition to discover that. Every one of these is a five-minute fix mis-diagnosed into an incident.
Who hits this hardest: CI/CD service principals and managed identities, whose permissions are set once and drift as the infra grows; newly onboarded engineers waiting on propagation lag; teams under landing zones, Blueprints or managed applications that introduce deny assignments the app team never sees; and anyone confusing control-plane (manage the resource) with data-plane (read the data inside) — the Storage Reader who sees the account but gets AuthorizationFailed listing blobs, because blob data needs Storage Blob Data Reader. The fix is almost never “give them Owner” — it’s “find the missing action or the deny, and grant the one role that supplies it at the narrowest scope.”
To frame the field before the deep dive, here is every root-cause class this article covers, the question it forces, and the first place to look:
| Root-cause class | What’s actually wrong | First question to ask | First place to look |
|---|---|---|---|
| No role assignment | Principal has no role granting the action at any covering scope | Does the principal have any assignment that covers this scope? | az role assignment list --assignee <id> --all |
| Wrong scope | Role exists but at a scope that doesn’t cover the target | Is the assignment at/above the resource, or off to the side? | Check access blade → the resource’s IAM |
| Propagation lag | Assignment is correct but not yet effective | Was the role granted in the last few minutes? | Re-auth + wait; Activity log timestamp |
| Deny assignment | A deny overrides the role you do have | Is there a deny from a managed app / Blueprint / lock? | az role assignment list --include-inherited + Deny assignments blade |
| NotActions subtraction | The role excludes this specific action | Does the role’s NotActions list the failing action? |
az role definition list --name <role> |
| Stale token | Token was minted before the new role | Did it work after you signed out and back in? | az account get-access-token / re-login |
| Control vs data plane | You have the management role, the call needs a data role | Is this action on the resource or on the data inside it? | The action string: .../write vs .../blobServices/.../read |
| ABAC condition fails | A conditional assignment’s condition isn’t met | Does the assignment carry a condition you don’t satisfy? | az role assignment list --query "[].condition" |
| Resource lock | A CannotDelete/ReadOnly lock blocks the write |
Is the error really about a lock, not a role? | az lock list --resource-group <rg> |
Learning objectives
By the end of this article you can:
- Parse an
AuthorizationFailedmessage into its three load-bearing parts — principal object id, action, and scope — and state the single hypothesis each one points to. - List a principal’s effective permissions with
az role assignment list --assignee <id> --all --include-inheritedand read the Check access / Effective access blade to see every assignment that does (or doesn’t) cover a target scope. - Distinguish a missing grant from an overriding deny assignment, and find the deny’s source (managed application, Blueprint, or system) when one exists.
- Decode a role definition’s
Actions,NotActions,DataActionsandNotDataActionswithaz role definition list, and explain whyContributorcannot assign roles and why aReadercannot read blob data. - Choose the correct scope (management group → subscription → resource group → resource) for a fix and assign the least-privilege built-in role that supplies exactly the failing action.
- Diagnose the control-plane vs data-plane trap and the AAD/Entra token propagation gotcha that make a correct assignment look broken for a few minutes.
- Recognise the lookalikes — resource locks, Azure Policy
deny, scope-is-invalid typos, and cross-tenant guest gaps — that surface asAuthorizationFailed(or near it) but are not RBAC grant problems, and confirm each with one command.
Prerequisites & where this fits
You should already understand the RBAC mental model: a role assignment binds a security principal (a user, group, service principal, or managed identity, identified by an Entra object id) to a role definition (a named set of permitted actions) at a scope (a node in the Azure resource hierarchy). You should know how to run az in Cloud Shell, read JSON output, and that scope is hierarchical — a role granted at a resource group applies to every resource in it. Familiarity with Azure Resource Hierarchy Explained: Subscriptions, Resource Groups and Resources makes the scope reasoning automatic, and Build Custom RBAC Roles: Actions, NotActions, and DataActions for Least Privilege covers the action grammar this article reads constantly.
This sits in the Identity & Troubleshooting track. It assumes the fundamentals above and pairs tightly with three neighbours. Management Groups 101: Designing a Hierarchy That Scopes Policy and RBAC is where inherited assignments and many deny sources originate. Azure Policy Effects Decoded: Deny vs Audit vs Modify vs DeployIfNotExists explains the other “you can’t do that” — a Policy deny, which looks similar but is a different subsystem with a different fix. And Managed Identities Demystified: System vs User-Assigned and When to Use Each covers the identities that hit this error most in automation. For the data-plane variant on secrets, Key Vault RBAC vs Access Policies: Choosing and Migrating the Permission Model is the canonical case.
A quick map of who or what owns each layer of an authorization decision, so you escalate to the right place fast:
| Layer | What lives here | Who usually owns it | Failure classes it can cause |
|---|---|---|---|
| Entra ID (identity) | The principal, its object id, group membership | Identity team | Wrong principal, guest gaps, group not yet effective |
| Token (AAD) | The signed claims the call presents | Azure CLI / the runtime | Stale token predating a new role assignment |
| Role assignment | Principal ↔ role ↔ scope binding | Resource/sub Owner or User Access Admin | No assignment, wrong scope, propagation lag |
| Role definition | The Actions/NotActions the role permits | Microsoft (built-in) or platform team (custom) | NotActions subtraction; wrong role chosen |
| Deny assignment | An explicit deny that overrides allows | Managed app / Blueprint / system | Hard block even with a granting role |
| Resource lock | CannotDelete / ReadOnly on the resource |
Resource owner | Write/delete blocked, looks like a role issue |
| Azure Policy | deny effect on the request body |
Governance team | Request blocked at admission, not by RBAC |
Core concepts
Five mental models make every later diagnosis obvious.
The error names what the engine checked, not where your access “should” come from. Every control-plane call carries a principal (an Entra object id) and targets a scope (a resource id). ARM asks: across all role assignments this principal has at this scope and every scope above it, is there one whose Actions include the requested action — with no NotActions excluding it and no deny assignment forbidding it? If yes, allow; otherwise AuthorizationFailed. The message hands you all three inputs, so your entire job is to reproduce that evaluation by reading the principal’s effective permissions.
Allow is additive and inherited; deny is absolute. Grants flow downward — a role at a management group applies to every subscription, resource group and resource beneath it; a role at a resource group, to every resource in it. You accumulate the union of every assignment at the target scope and all ancestors — no ordering, no precedence. The one thing that breaks the additive model is a deny assignment: an explicit “this principal may NOT perform this action here,” which overrides any allow no matter how high the granting role sits. You can’t create deny assignments in the portal — they come from Azure managed applications, Blueprints (deprecated but still present), and a few system protections. Have a granting role and still get denied? A deny assignment is the prime suspect.
A role is Actions minus NotActions, on two planes. A role definition is four lists. Actions are the control-plane operations it permits. NotActions are operations subtracted — Contributor has * in Actions but lists Microsoft.Authorization/*/Write, Microsoft.Authorization/*/Delete and Microsoft.Authorization/elevateAccess/Action in NotActions, which is exactly why a Contributor can do almost anything except grant roles. DataActions/NotDataActions are the same idea for the data plane — operations on the data inside a resource — kept separate so you can manage a storage account without reading its data, or read the data without managing the account. Crucially, NotActions are subtractions, not denies: another role can add the action back. Only a deny assignment is absolute.
Scope is a path, and “covers” means ancestor-or-self. Scopes are slash-delimited ids forming a tree: managementGroups/<mg> → /subscriptions/<sub> → .../resourceGroups/<rg> → .../providers/<rp>/<type>/<name>. An assignment covers a target if its scope is the target or an ancestor of it. A role at rg-prod covers a VM inside it; the same role at rg-staging covers nothing in rg-prod — sibling, not ancestor. The single most common “but I have access!” is a real assignment at the wrong scope. The other half of “or the scope is invalid” is a literal malformed/typo’d id.
Control plane and data plane are different doors with different keys. ARM (management.azure.com) is the control plane — create/configure/delete the resource. The resource’s own endpoint (blob.core.windows.net, vault.azure.net) is the data plane — read/write the contents. They authorize separately: Storage Account Contributor manages the account but grants no blob data access; a Reader on an RBAC-mode Key Vault sees the vault but can’t read a secret value until granted Key Vault Secrets User. When the failing action contains a data path (/blobServices/, /secrets/, /queueServices/), you need a data role — management roles won’t help no matter how broad.
The vocabulary in one table
Before the deep sections, pin down every moving part. The glossary repeats these for lookup; this is the mental model side by side:
| Concept | One-line definition | Where it lives | Why it matters to AuthorizationFailed |
|---|---|---|---|
| Security principal | User, group, SPN, or managed identity (an object id) | Entra ID | The “who”; the error’s object id |
| Role definition | Named set of allowed actions | Subscription / tenant | The Actions/NotActions evaluated |
| Role assignment | Principal ↔ role ↔ scope binding | At a scope | The grant that’s present or missing |
| Scope | A node in the resource hierarchy | MG / sub / RG / resource | Must be ancestor-or-self of the target |
| Action | One operable verb on a resource type | Inside the role | The exact thing the call needed |
| NotActions | Actions subtracted from a role | Inside the role | Why Contributor can’t assign roles |
| DataActions | Data-plane operations a role permits | Inside the role | Why a Reader can’t read blob data |
| Deny assignment | Explicit deny overriding allows | Managed app / Blueprint / system | Blocks even a granted role |
| Propagation | Time for an assignment to take effect | Platform behaviour | A correct grant fails for minutes |
| Effective access | The union of all assignments at a scope | Portal blade / az query |
What you read to reproduce the decision |
| Control plane | Manage the resource via ARM | management.azure.com |
Needs Actions |
| Data plane | Read/write the data inside | The resource’s own endpoint | Needs DataActions |
Reading the AuthorizationFailed message
The message is a structured record, not a wall of noise. Learn to read its fields and you have your hypothesis before you run anything. The canonical shape:
(AuthorizationFailed) The client '<upn-or-appId>' with object id '<objectId>'
does not have authorization to perform action '<provider/type/operation>'
over scope '<resourceId>' or the scope is invalid.
If access was recently granted, please refresh your credentials.
Three fields carry the entire diagnosis. Read them in this order:
| Field in the message | What it tells you | The hypothesis it points to | Confirm it with |
|---|---|---|---|
object id '<objectId>' |
The exact principal ARM evaluated | Are you even running as who you think? (SPN vs user vs MI) | az ad signed-in-user show / az account show |
action '<...>' |
The exact operation required | Which role supplies this action, on which plane | az provider operation show --namespace <ns> |
over scope '<...>' |
The exact target scope | Is there a covering assignment at/above it | az role assignment list --scope <id> --include-inherited |
| “the scope is invalid” | The id may be malformed | Typo / wrong subscription / deleted resource | Echo the id; az resource show --ids <id> |
| “recently granted… refresh” | Propagation/token hint | Stale token or not-yet-propagated assignment | Re-login; wait; recheck |
The two halves of “or the scope is invalid” matter. If the scope id is well-formed and the resource exists, treat it as a permissions problem and go read effective access. If the id is malformed (a missing segment, a wrong subscription guid, a resource you already deleted), it is a scope problem — fix the id, no role change needed. Echo the scope and eyeball it before assuming RBAC.
The action string also tells you which plane and which role family you need. Mapping the action shape to the fix:
| Action string contains… | It’s a… | Role family to look in | Example role |
|---|---|---|---|
.../write, .../delete, .../action on the resource type |
Control-plane management op | Built-in management roles | Contributor, Storage Account Contributor |
Microsoft.Authorization/roleAssignments/write |
A grant operation | Access-management roles only | Owner, User Access Administrator, RBAC Administrator |
.../blobServices/.../read, /queueServices/, /tableServices/ |
Data-plane storage op | Storage Data roles | Storage Blob Data Reader/Contributor |
Microsoft.KeyVault/vaults/secrets/... (data) |
Data-plane Key Vault op | Key Vault data roles (RBAC mode) | Key Vault Secrets User |
.../read only, and the call is a list/show |
A read op | Any role with */read |
Reader |
Microsoft.Authorization/elevateAccess/Action |
The break-glass elevation | Reserved (Global Admin path) | n/a (special) |
The closing line — “If access was recently granted, please refresh your credentials” — is a genuine hint, not boilerplate: it flags the two time-dependent causes (a stale token or propagation lag). If the assignment exists when you check but the call still failed, that sentence is usually the answer.
Reading effective permissions: the core skill
Everything downstream is one of two readings — the principal’s assignments, or a scope’s effective access. Master both commands and the portal equivalents and you can confirm any cause in under a minute.
List a principal’s assignments
Start from the principal in the error. --all searches every scope (not just the current one); --include-inherited pulls in assignments from ancestor scopes that flow down:
# Replace with the object id from the error message
PRINCIPAL=a1b2c3d4-1111-2222-3333-444455556666
# Every assignment this principal has, at any scope, including inherited
az role assignment list --assignee "$PRINCIPAL" --all --include-inherited \
--query "[].{role:roleDefinitionName, scope:scope, type:principalType}" -o table
Read the output against the failing scope: is there a row whose scope is the target or an ancestor of it, whose role plausibly contains the failing action? If no row covers the scope → no assignment / wrong scope. If a row covers it but the role shouldn’t supply that action → check the role definition (NotActions, or wrong plane).
Confirm you’re running as who you think first — the object id in the error is authoritative, and SPNs, managed identities and your user are different objects:
az ad signed-in-user show --query "{upn:userPrincipalName, id:id}" -o table
az account show --query "{sub:name, subId:id, tenant:tenantId, user:user.name}" -o table
# If the error names an app/SPN, resolve its object id:
az ad sp show --id <appId-or-objectId> --query "{name:displayName, objId:id}" -o table
A frequent shock: the failing principal is a service principal or managed identity — a pipeline or a Function’s identity — so checking your own access proves nothing. Always diagnose the principal named in the error.
Read a scope’s effective access (Check access)
The other direction: stand at the target resource and ask “does this principal cover it?” In the portal that’s Resource → Access control (IAM) → Check access → (pick the principal), showing every assignment that applies there, including inherited ones with their source scope. The CLI equivalent, scoped to the resource:
SCOPE="/subscriptions/<sub>/resourceGroups/rg-prod/providers/Microsoft.Storage/storageAccounts/stprod001"
# Every assignment effective at this scope (inherited included), filtered to the principal
az role assignment list --scope "$SCOPE" --include-inherited --assignee "$PRINCIPAL" \
--query "[].{role:roleDefinitionName, scope:scope}" -o table
The scope column is the punchline: it tells you where each effective assignment is anchored. A row anchored at the subscription means the grant is inherited from above; an empty result means nothing covers this principal here — the missing-grant case. The portal’s Effective access / Check access view renders the same union with a friendlier source-scope label.
Confirm what action a role actually contains
When a role should cover the action but the call still failed, read the role definition. This is where NotActions subtractions and plane mismatches reveal themselves:
# Dump a role's four action lists
az role definition list --name "Contributor" \
--query "[0].permissions[0].{actions:actions, notActions:notActions, dataActions:dataActions, notDataActions:notDataActions}" -o json
For Contributor this prints actions: ["*"] and notActions: ["Microsoft.Authorization/*/Write", "Microsoft.Authorization/*/Delete", "Microsoft.Authorization/elevateAccess/Action", "Microsoft.Blueprint/blueprintAssignments/write", "Microsoft.Blueprint/blueprintAssignments/delete", "Microsoft.Compute/galleries/share/action"] — proof that “everything except granting roles (and a few specifics)” is literal. Map your failing action against the notActions list: if it’s there, this role will never authorise it and you need a different role that supplies it.
Confirm the action exists and which plane it’s on
To learn whether an action is control-plane or data-plane, and which built-in roles include it, query the provider’s operation catalogue:
# All operations for a provider, with their display text
az provider operation show --namespace Microsoft.Storage \
--query "resourceTypes[?name=='storageAccounts/blobServices/containers/blobs'].operations[].{op:name, display:displayName, isData:isDataAction}" -o table
The isDataAction flag is the tell: true means the operation lives on the data plane and is authorised by DataActions (so you need a Data role), not management Actions. This single query resolves most control-vs-data confusion. To recap the four readings: query by principal (--assignee ... --all --include-inherited) for all its grants; by scope (--scope ... --include-inherited, or Check access) for what covers a target; by role (az role definition list) for what actions it permits or excludes; and by action (az provider operation show) for whether it’s data-plane and which roles carry it.
The deny assignment trap
If the principal has a granting role at a covering scope and the call still returns AuthorizationFailed, you’re almost certainly looking at a deny assignment — an explicit “may NOT perform this action” that overrides every allow. It’s the most confusing RBAC failure because the obvious check (does it have a role?) comes back yes. Deny assignments aren’t created through Add role assignment; they’re emitted by a small set of system features:
| Deny-assignment source | Why it exists | How to recognise it | Can you remove it directly? |
|---|---|---|---|
| Azure managed application | Publisher locks the managed resource group so consumers can’t tamper | Deny on a *-managed-rg; principals = everyone except the app’s identity |
No — managed by the app; delete the managed app to remove |
| Azure Blueprints (deprecated) | Blueprint assignment with denyAssignment locking artifacts |
Deny tied to a Blueprint assignment | Remove/loosen the Blueprint assignment |
| System (platform) | Built-in protections on certain platform-managed resources | System-principal deny | No — platform-owned |
Read deny assignments in their own surface — Resource → Access control (IAM) → Deny assignments lists every deny touching the scope, the principals it applies to, and the actions it denies (its own NotActions are the exceptions — actions NOT denied). If your failing action is denied and your principal is in scope (and not excluded), that’s your answer. The crucial reframe: you don’t “grant around” a deny — adding a higher role does nothing. Address the source (use the managed app’s supported interface; loosen a Blueprint’s locking). Allow vs deny, made explicit:
| Property | Allow (role assignment) | Deny (deny assignment) |
|---|---|---|
| Created via | Add role assignment (you, an Owner) | Managed app / Blueprint / system only |
| Combine rule | Additive union across scopes | Overrides all allows at/above scope |
| Removable by you | Yes, if you have access-mgmt rights | Usually no — address the source |
| Shows up in | IAM → Role assignments | IAM → Deny assignments |
| Fix when it bites | Grant the right role/scope | Use the supported interface / remove the source |
NotActions, scope and the “I’m Contributor, why can’t I?” cases
Two failures dominate the “but I clearly have rights” complaints, and both are reading problems, not missing-grant problems.
NotActions subtraction — the Contributor-can’t-grant-roles case
A role with Actions: ["*"] looks all-powerful until you read its NotActions. Contributor is deliberately “manage everything except authorization,” so holders can’t escalate by granting themselves Owner. This trips up automation constantly — a pipeline running as Contributor tries to assign a role to a managed identity (a normal IaC step) and gets:
(AuthorizationFailed) ... does not have authorization to perform action
'Microsoft.Authorization/roleAssignments/write' over scope '/subscriptions/.../resourceGroups/rg-app'
The action is Microsoft.Authorization/roleAssignments/write, which sits squarely in Contributor’s NotActions. Confirm and fix by choosing a role that does supply it — User Access Administrator (or the newer Role Based Access Control Administrator, which is more constrained), scoped narrowly:
# Confirm Contributor excludes the action
az role definition list --name "Contributor" --query "[0].permissions[0].notActions" -o json
# -> includes "Microsoft.Authorization/*/Write" -> roleAssignments/write is excluded
# Grant the pipeline the ability to manage role assignments at the RG only (least privilege)
az role assignment create --assignee "$PRINCIPAL" \
--role "Role Based Access Control Administrator" \
--scope "/subscriptions/<sub>/resourceGroups/rg-app"
The common built-in roles that can assign roles, and how they differ:
| Role | Can assign roles? | Constraint | When to use |
|---|---|---|---|
| Owner | Yes — any role | Full control of everything else too | Avoid for automation; too broad |
| User Access Administrator | Yes — any role | Manages access but not resources | Classic “let this principal grant roles” |
| Role Based Access Control Administrator | Yes, but constrained | Can be limited to specific roles via condition; cannot assign privileged roles to itself freely | Preferred least-privilege grant for pipelines |
| Contributor | No | Microsoft.Authorization/*/Write in NotActions |
Manage resources, never grant roles |
Wrong scope — the right role in the wrong place
The other dominant case: the principal genuinely has the role, but at a scope that doesn’t cover the target. A Contributor on rg-staging cannot touch rg-prod; an Owner on Subscription A cannot act in Subscription B. Because the assignment exists, az role assignment list --assignee shows a reassuring row — you have to check the scope column against the failing scope.
# Does ANY of the principal's assignments cover this exact scope (or an ancestor)?
TARGET="/subscriptions/<subB>/resourceGroups/rg-prod/providers/Microsoft.Web/sites/app-prod"
az role assignment list --assignee "$PRINCIPAL" --all --include-inherited \
--query "[?contains('$TARGET', scope)].{role:roleDefinitionName, scope:scope}" -o table
# Empty -> nothing covers the target. Non-empty -> something does; look elsewhere (NotActions/plane/deny).
The fix is to assign at the smallest scope that covers the target — and scope choice is the least-privilege decision. Default to the single resource (tightest) or resource group (the sensible default when a principal owns a workload’s RG); reserve the subscription for platform/landing-zone operators and audit it; use the management group only for genuine org-wide roles, since a grant there is inherited by every subscription beneath it.
Control plane vs data plane
The trap that eats the most senior-engineer hours: you have a perfectly good management role and the operation needs a data role. The two planes authorise independently — ARM (management.azure.com) authorises control-plane actions via a role’s Actions, while the resource’s own endpoint (blob.core.windows.net, vault.azure.net) authorises data-plane operations via DataActions. Management roles like Contributor and Storage Account Contributor carry no DataActions, so they grant zero data access by design — exactly what you want for an operator who should run a resource without reading its contents.
The storage case is canonical: listing blobs “as the account” works only with a blob data role; otherwise it’s AuthorizationFailed/403 even as the account’s Owner (unless account-key access papers over it, an anti-pattern). The fix is a data role at the account/container scope:
# Symptom: can see the account, can't list blobs. Confirm the action is data-plane:
az provider operation show --namespace Microsoft.Storage \
--query "resourceTypes[?contains(name,'blobServices/containers/blobs')].operations[?isDataAction].name" -o tsv | head
# Grant the data-plane role at the account scope (container scope for tighter control)
az role assignment create --assignee "$PRINCIPAL" \
--role "Storage Blob Data Reader" \
--scope "/subscriptions/<sub>/resourceGroups/rg-prod/providers/Microsoft.Storage/storageAccounts/stprod001"
The same split shows up across services. The management role manages the resource; the data role touches the contents:
| Service | Control-plane role (manage the resource) | Data-plane role (touch the data) | The error when you confuse them |
|---|---|---|---|
| Storage (Blob) | Storage Account Contributor | Storage Blob Data Reader / Contributor | Can’t list/read blobs despite owning the account |
| Key Vault (RBAC mode) | Contributor / Key Vault Contributor | Key Vault Secrets User / Crypto User | Can see the vault, can’t read a secret value |
| Cosmos DB | DocumentDB Account Contributor | Cosmos DB Built-in Data Reader/Contributor (data-plane RBAC) | Can manage the account, can’t read items |
| Service Bus | Contributor / Azure Service Bus Data Owner (mgmt parts) | Azure Service Bus Data Sender / Receiver | Can manage the namespace, can’t send/receive |
| Storage (Queue) | Storage Account Contributor | Storage Queue Data Contributor | Can’t read queue messages |
Two rules make this less mysterious. Read the action string — a data path (/blobServices/, /secrets/, an isDataAction: true op) means a data role, full stop. And for Key Vault, confirm the vault is in RBAC mode: if it’s on legacy access policies, RBAC roles are ignored entirely and you grant via access policies instead — the whole subject of Key Vault RBAC vs Access Policies: Choosing and Migrating the Permission Model.
Propagation, tokens and ABAC conditions
Three time- and condition-dependent causes make a correct assignment look broken. Recognise them so you don’t “fix” something that was already right.
Propagation lag
A new role assignment is not instant — ARM replicates it and caches refresh, typically within a couple of minutes but longer across regions or for group-based grants (where two propagations stack: group membership, then the assignment). The error says so: “If access was recently granted, please refresh your credentials.”
# Verify the assignment actually exists now (separate "does it exist" from "is it effective")
az role assignment list --assignee "$PRINCIPAL" --scope "$SCOPE" \
--query "[].{role:roleDefinitionName, created:createdOn}" -o table
If the assignment shows up with a recent createdOn, it exists — the failure is propagation/token, not a missing grant. Wait, re-authenticate, and retry. Do not add a second, broader role to “force” it; you’ll forget to remove it.
Stale token
A bearer token carries the principal’s claims as of when it was issued and is valid for its lifetime (commonly up to an hour). A token minted before the new assignment (or before the group membership that confers it) doesn’t carry the new access until refreshed — even though the assignment is live server-side. This is why “log out and back in” genuinely fixes things, and the same holds for SPNs and managed identities (their cached token must refresh):
# Force a fresh token so new assignments / group membership are reflected
az logout && az login
# In a pipeline, re-run the SPN login step to re-acquire the token.
Group membership changes are the worst offender — only reflected in a newly issued token, so a long-lived session keeps using old claims until it renews.
ABAC conditions
ABAC (Attribute-Based Access Control) lets a role assignment carry a condition — extra constraints (e.g. “only blobs tagged project=x”). If a conditional assignment is the only thing granting your action and the condition isn’t satisfied, you get AuthorizationFailed even though the role and scope are right. Check for conditions:
az role assignment list --assignee "$PRINCIPAL" --all \
--query "[?condition != null].{role:roleDefinitionName, scope:scope, condition:condition}" -o json
If a condition is present, read it: your request must match its attributes (resource tags, blob path, request attributes) to be allowed. The fix is to satisfy the condition (act on a matching resource) or, if the condition is wrong, edit it. The three time/condition causes side by side:
| Cause | Tell-tale | Confirm | Fix (do NOT broaden the role) |
|---|---|---|---|
| Propagation lag | Just granted; assignment exists but call fails | createdOn is minutes ago |
Wait, re-auth, retry |
| Stale token | Worked after sign-out/in; long session | Token issued before the grant | az logout && az login / refresh SPN token |
| Group membership not effective | Granted via a group you just joined | Membership recent; old token in use | New token (re-login) reflects membership |
| ABAC condition fails | Assignment carries a condition |
--query "[?condition != null]" |
Match the condition or edit it |
Architecture at a glance
There is no diagram to study here — hold this mental model instead, because the shape of an authorization decision is what makes the playbook obvious. Picture the request as a single straight line with one gate on it. Your call leaves a principal (an Entra object id) carrying a token of signed claims and arrives at Azure Resource Manager addressed to a scope asking for an action. ARM is the gate, evaluating one question: across every role assignment this principal holds at the target scope and every ancestor above it — resource → resource group → subscription → management group — does the union of their Actions include the requested action (minus any NotActions), with no deny assignment forbidding it and any ABAC condition satisfied? Allow flows down and accumulates; deny sits on top and overrides.
So every AuthorizationFailed is one of two failures at that gate: the union of allows didn’t contain the action at a covering scope (missing grant, wrong scope, NotActions subtraction, wrong plane, or a not-yet-effective grant), or a deny above the allows vetoed it. Your diagnosis walks the line in ARM’s own order: confirm which principal the token represents, walk scopes from the resource upward for a covering allow, check for an overriding deny, then — if an allow exists and no deny does — suspect time (propagation/stale token) or plane (control vs data). The whole method is “reproduce ARM’s evaluation by reading effective access from the resource upward.”
Real-world scenario
Vantix Logistics runs a fleet-telemetry platform on Azure: a set of Functions and an AKS workload writing to Cosmos DB and Blob Storage, deployed entirely by a Terraform pipeline running as a single service principal (sp-vantix-cicd) in Central India. The platform team is five engineers; everything ships through CI under a landing zone the central cloud team manages, with subscriptions sitting under a mg-workloads management group.
The incident began on a Tuesday release. The pipeline had been green for months, then a routine terraform apply — adding a new storage account and granting the Functions’ managed identity a data role on it — failed at the role-assignment step:
(AuthorizationFailed) The client 'sp-vantix-cicd' with object id 'b7...'
does not have authorization to perform action 'Microsoft.Authorization/roleAssignments/write'
over scope '/subscriptions/.../resourceGroups/rg-telemetry-prod'.
The on-call engineer assumed a propagation blip and re-ran — same failure — then suspected the SPN had lost permissions. A check said otherwise: az role assignment list --assignee b7... --all showed sp-vantix-cicd holding Contributor at the subscription, exactly as designed. So it had a role, yet the call failed — the shape that screams deny assignment but here meant something simpler.
The breakthrough came from reading the action, not the role. The failing action was Microsoft.Authorization/roleAssignments/write; dumping az role definition list --name Contributor --query "[0].permissions[0].notActions" showed Microsoft.Authorization/*/Write. Contributor intentionally cannot grant roles. Every prior pipeline run only created resources; the new Terraform that wired the Functions’ managed identity to a Storage data role was the first time the SPN tried to write a role assignment — and Contributor said no. The gap had been invisible for months.
The team’s first instinct was to ask for Owner at the subscription — quick, unblocks the release. The lead pushed back: Owner on a CI principal is a standing escalation path and an audit finding. Instead they granted the narrower Role Based Access Control Administrator at the resource-group scope only, with a condition limiting it to the storage data roles the pipeline needed — supplying exactly roleAssignments/write where required and nowhere else. The re-run then hit a third issue: the managed identity could write to the account but not read blobs, because the pipeline had granted Storage Account Contributor (control plane) instead of Storage Blob Data Contributor (data plane). Correcting the Terraform cleared it; the release shipped ninety minutes after the first failure. The retro put three lines on the wall — “Read the action, not the role.” “Contributor never grants roles.” “Account roles aren’t data roles.” — and added a pipeline pre-flight that asserts the SPN holds RBAC-admin at the workload RG before any role-assignment step, so the next new workload fails fast instead of mid-release.
The incident as a timeline, because the order of moves is the lesson:
| Time | Symptom | Action taken | Effect | What it should have been |
|---|---|---|---|---|
| 10:02 | roleAssignments/write AuthorizationFailed |
Re-run the pipeline | Same failure | Read the action string first |
| 10:08 | Still failing | Check SPN’s assignments | Contributor at sub — “it has access!” | Don’t stop at “role exists” |
| 10:20 | Confusion (has role, still denied) | Dump Contributor notActions |
Authorization/*/Write excluded — the answer |
This is the breakthrough |
| 10:35 | Root cause found | Proposed: Owner at subscription | Would work, over-privileged | Don’t grant Owner to CI |
| 10:45 | Mitigated | RBAC Admin at RG (conditioned) | roleAssignments/write supplied narrowly |
Correct least-privilege fix |
| 11:05 | Re-run hits blob read failure | Granted Storage Account Contributor | Still can’t read blobs | Needed a data role |
| 11:25 | Fixed | Switch to Storage Blob Data Contributor | Release ships | Account role ≠ data role |
Advantages and disadvantages
The “deny-by-default, additive-allow, hierarchical-scope” model both causes this error and makes it diagnosable. Weigh it honestly:
| Advantages (why the model helps you) | Disadvantages (why it bites) |
|---|---|
| The error names the exact principal, action and scope — the diagnosis is handed to you | The bare string reveals nothing about which role would work; you must read effective access |
| Allow is additive and inherited — grant once high up, it flows down to everything beneath | Inheritance hides where a grant comes from; “I have access” can be an assignment three scopes up |
| Least privilege is expressible precisely — narrow scope + specific role + ABAC condition | Precision means more ways to be just short — wrong scope, NotActions, data vs control plane |
| Deny assignments give hard, un-escalatable guardrails for managed apps and platforms | A deny is invisible in the normal IAM list and can’t be granted around — confusing when you have a role |
| Control/data-plane separation lets you manage a resource without reading its data | The same separation surprises people: account Owner who can’t read a blob |
Reader/Contributor/Owner plus hundreds of granular built-ins cover most needs |
Picking the right built-in among hundreds is itself a skill; the obvious one is often too broad |
| Propagation and token caching are normal distributed-system behaviour | A correct grant fails for minutes; the time-based causes mimic a real permission gap |
It bites hardest on CI/CD principals (permissions drift; the Contributor-can’t-grant-roles wall), data-plane workloads (the storage/Key Vault/Cosmos data-role surprise), and anyone under a landing zone where deny assignments and inherited grants originate out of view. Every disadvantage is manageable — but only if you know it exists.
Hands-on lab
Reproduce a real AuthorizationFailed, read effective permissions to localise it, and fix it with the narrowest role — all free (role assignments and reads cost nothing; we create one cheap storage account and delete it). Run in Cloud Shell (Bash). You’ll need rights to create a resource group, a storage account, and role assignments (Owner or User Access Administrator on a sandbox subscription).
Step 1 — Variables and a resource group.
RG=rg-rbac-lab
LOC=centralindia
ST=strbaclab$RANDOM # globally-unique, lowercase, no dashes
SUB=$(az account show --query id -o tsv)
az group create -n $RG -l $LOC -o table
Step 2 — Create a storage account and a test service principal (our “victim” principal).
az storage account create -n $ST -g $RG -l $LOC --sku Standard_LRS -o table
# A throwaway SPN with NO role yet — this is the principal we'll diagnose
APPID=$(az ad sp create-for-rbac -n sp-rbac-lab --skip-assignment --query appId -o tsv)
OBJID=$(az ad sp show --id $APPID --query id -o tsv)
echo "SPN appId=$APPID objectId=$OBJID"
Expected: an account row, and an SPN created without any assignment (--skip-assignment).
Step 3 — Reproduce the failure: confirm the SPN has nothing. List the principal’s assignments — it should be empty:
az role assignment list --assignee $OBJID --all --include-inherited \
--query "[].{role:roleDefinitionName, scope:scope}" -o table
# Expected: empty. With zero assignments, ANY action by this SPN -> AuthorizationFailed.
That’s the “no role assignment” root cause in its purest form — a principal with no covering grant.
Step 4 — Grant the WRONG thing on purpose (control plane), then prove the data-plane gap. Give the SPN Reader on the account, then check what blob data it can do:
az role assignment create --assignee $OBJID --role "Reader" \
--scope "/subscriptions/$SUB/resourceGroups/$RG/providers/Microsoft.Storage/storageAccounts/$ST"
# Reader covers control-plane reads, but these blob DATA actions are NOT in it ->
# reading blob data would still AuthorizationFailed/403. Right plane is everything.
az provider operation show --namespace Microsoft.Storage \
--query "resourceTypes[?contains(name,'blobServices/containers/blobs')].operations[?isDataAction].name" -o tsv | head -3
Step 5 — Fix it with the narrowest correct role. Grant the data-plane role at the account scope:
az role assignment create --assignee $OBJID --role "Storage Blob Data Reader" \
--scope "/subscriptions/$SUB/resourceGroups/$RG/providers/Microsoft.Storage/storageAccounts/$ST"
# Confirm BOTH roles now cover the account: Reader + Storage Blob Data Reader.
az role assignment list --assignee $OBJID --all --include-inherited \
--query "[].{role:roleDefinitionName, scope:scope}" -o table
The SPN can now read the account and its blob data, with no broader grant.
Step 6 — Read it the way you would in an incident (Check access). Reproduce the portal’s “who covers this scope?” reading:
SCOPE="/subscriptions/$SUB/resourceGroups/$RG/providers/Microsoft.Storage/storageAccounts/$ST"
az role assignment list --scope "$SCOPE" --include-inherited --assignee $OBJID \
--query "[].{role:roleDefinitionName, anchoredAt:scope}" -o table
# anchoredAt shows the account scope — exactly what you'd read mid-incident.
Validation checklist. You reproduced AuthorizationFailed from a zero-grant principal, saw a control-plane role fail to supply data-plane access, fixed it with the narrowest data role at the resource scope, and read effective access both by-principal and by-scope. No Owner, no subscription-wide grant. The steps mapped to what each proves:
| Step | What you did | What it proves | Real-world analogue |
|---|---|---|---|
| 3 | List a zero-grant SPN | “No assignment” → AuthorizationFailed | A brand-new CI principal |
| 4 | Reader on the account, check data ops | Control-plane role ≠ data access | The storage blob-read surprise |
| 5 | Add Storage Blob Data Reader | The narrow data role is the fix | The actual least-privilege grant |
| 6 | Read effective access by scope | How to confirm coverage mid-incident | The 60-second diagnosis |
Cleanup.
az role assignment delete --assignee $OBJID --scope "/subscriptions/$SUB/resourceGroups/$RG" 2>/dev/null
az ad sp delete --id $APPID
az group delete -n $RG --yes --no-wait
Cost note. Role assignments and IAM reads are free. The Standard_LRS account holds no data here, so it costs effectively nothing for the lab’s lifetime; deleting the resource group and SPN removes everything.
Common mistakes & troubleshooting
This is the playbook — the part you bookmark. First as a scannable table you read the moment the error fires, then the same entries with the full confirm-command detail underneath. It spans the basic gaps (no grant, wrong scope, stale token) and the advanced ones (deny assignments, NotActions, ABAC, cross-tenant, plane confusion).
| # | Symptom | Root cause | Confirm (exact cmd / portal path) | Fix |
|---|---|---|---|---|
| 1 | AuthorizationFailed and the principal has no assignment covering the scope |
No role assignment at/above the target | az role assignment list --assignee <id> --all --include-inherited → empty for the scope |
Assign the least-privilege role at the narrowest covering scope |
| 2 | Principal has the role but still denied; assignment is at a sibling scope | Right role, wrong scope (sibling, not ancestor) | Check the scope column vs the failing scope; Check access blade |
Re-assign at the resource/RG that actually covers the target |
| 3 | Just granted; assignment exists but call still fails | Propagation lag / stale token | createdOn is minutes ago; sign out/in fixes it |
Wait, re-authenticate, retry — don’t add a broader role |
| 4 | Has a granting role, yet still AuthorizationFailed |
Deny assignment overrides the allow | IAM → Deny assignments; managed-RG / Blueprint deny | Use the supported interface; remove/loosen the source (can’t grant around it) |
| 5 | Contributor can’t assign a role to a managed identity | roleAssignments/write is in Contributor’s NotActions |
az role definition list --name Contributor --query "[0].permissions[0].notActions" |
Grant RBAC Administrator / User Access Admin at the RG |
| 6 | Can see a storage account but can’t list/read blobs | Data-plane action needs a data role, not a mgmt role | Action contains /blobServices/; isDataAction:true |
Assign Storage Blob Data Reader/Contributor at account/container |
| 7 | Reader on a Key Vault (RBAC mode) can’t read a secret | Secret read is a data-plane op | Action .../vaults/secrets/...; vault in RBAC mode |
Grant Key Vault Secrets User; verify vault uses RBAC not access policies |
| 8 | Worked yesterday, fails today after a group change | Group membership only in a new token | Membership added recently; long-lived session | az logout && az login (new token reflects the group) |
| 9 | Error says “or the scope is invalid” and the id looks odd | Malformed/typo’d scope, wrong sub, deleted resource | Echo the id; az resource show --ids <id> |
Fix the resource id — not a role problem |
| 10 | Granted at a role that should work, action still missing | Chose a role whose Actions lack the op (or NotActions excludes it) |
az provider operation show; az role definition list --name <role> |
Pick a role that supplies the exact failing action |
| 11 | Conditional assignment present; some requests denied | ABAC condition not satisfied by the request | az role assignment list --query "[?condition!=null]" |
Match the condition (tags/path) or edit it |
| 12 | Guest/partner user denied across tenants | Cross-tenant identity not assigned in this tenant | Confirm the user is a guest here; check object id | Invite/assign the guest in this tenant, or use the right identity |
| 13 | Write/delete fails with a permissions-flavoured error, but role is fine | A resource lock (ReadOnly/CannotDelete), not RBAC |
az lock list --resource-group <rg> / on the resource |
Remove/override the lock; or expect read-only |
| 14 | ARM rejects the create even though RBAC allows it | Azure Policy deny on the request body (not RBAC) |
Activity log shows a policy denial with the policy name | Fix the request to comply, or get a policy exemption |
| 15 | elevateAccess or “can’t manage other admins” |
Trying a reserved/escalation action | Action is Microsoft.Authorization/elevateAccess/Action |
Use the proper break-glass/Global-Admin path; not a normal grant |
The table is the playbook; the entries below add the reasoning for the four that cause the most wasted hours — the ones where the obvious check misleads you.
4. The principal has a granting role yet is still denied (deny assignment). This is the most confusing case because the obvious check — “does it have a role?” — comes back yes. A deny assignment overrides the allow, and it comes from an Azure managed application locking its managed resource group, a Blueprint assignment, or a system protection — never the normal Add-role-assignment flow, which is why it’s invisible in the usual IAM list. Confirm at Resource → Access control (IAM) → Deny assignments: a deny whose actions include yours and whose principals include yours (and don’t exclude you). You cannot grant around it — adding a higher role does nothing. Act through the managed app’s supported interface, or remove/loosen the source.
5. A Contributor pipeline can’t assign a role (NotActions). Microsoft.Authorization/roleAssignments/write sits in Contributor’s NotActions by design, so a Contributor can’t escalate by granting itself Owner. Any IaC step that wires a role to a managed identity hits this the first time it runs. Confirm with az role definition list --name Contributor --query "[0].permissions[0].notActions" (it shows Microsoft.Authorization/*/Write). Fix by granting Role Based Access Control Administrator (preferred — condition-constrainable) or User Access Administrator at the workload resource group — not Owner, not at the subscription.
6 & 7. Can see the resource, can’t touch its data (control vs data plane). Blob and secret operations are data-plane (DataActions); management roles like Storage Account Contributor, Key Vault Contributor, even Owner, carry none — so you can manage the resource and still get AuthorizationFailed/403 on its contents. Confirm by the action string (/blobServices/, /vaults/secrets/..., or isDataAction: true via az provider operation show). Fix with a data role: Storage Blob Data Reader/Contributor or Key Vault Secrets User, scoped to the account/vault (or container/secret). For Key Vault, first confirm the vault is in RBAC mode — on legacy access policies, RBAC roles are ignored and you grant via access policies instead.
3 & 8. A correct grant that “isn’t working” (propagation / stale token). A just-made assignment is live server-side but not yet in caches or in the token the call presented — the error’s “recently granted… refresh” line points right at it. The worst variant is group-based: a membership change is reflected only in a newly issued token, so a long-lived session keeps using old claims. Confirm the assignment shows a recent createdOn (it exists), then re-authenticate (az logout && az login, or restart the runtime / re-acquire the SPN token) and retry. Never add a broader role to “force” it — you’ll leave an over-grant behind.
The remaining entries (1, 2, 9–15) are read straight off the table: a missing or wrong-scope assignment (read the scope column against the target), a malformed scope id (az resource show --ids to confirm it’s real), the wrong role for the action (az provider operation show + the role’s Actions), an unmet ABAC condition, a cross-tenant guest gap, and the two non-RBAC lookalikes — a resource lock (az lock list; see Azure Resource Locks: Prevent Accidental Deletes and Changes in Production) and an Azure Policy deny rejecting the request body at admission (the Activity log names the policy, not an AuthorizationFailed; see Azure Policy Effects Decoded: Deny vs Audit vs Modify vs DeployIfNotExists).
Best practices
- Read the error’s three fields first — principal, action, scope — before touching IAM. They hand you the hypothesis; running blind wastes the diagnosis Azure already gave you.
- Diagnose the principal named in the error, not yourself. It’s usually an SPN or managed identity; your own access proves nothing.
- Confirm coverage with
--include-inherited(or Check access), not just--assignee. A role can exist at the wrong scope and still show up — thescopecolumn is the truth. - Separate “does the assignment exist” from “is it effective.” A recent
createdOnplus a failing call means propagation/token — wait and re-auth instead of over-granting. - Grant at the narrowest scope that covers the target. Resource or RG by default; subscription rarely; management group only for genuine org-wide roles.
- Never hand Contributor a role-assignment job, and never reach for Owner to unblock automation. Use RBAC Administrator (condition-constrained) or User Access Administrator at the RG.
- Match the plane to the action. A data path (
/blobServices/,/secrets/) needs a data role; management roles grant zero data access by design. - Treat a deny assignment as un-grantable. Don’t pile on higher roles — address the managed app / Blueprint / lock that emitted it.
- Prefer groups for human access, per-identity assignments for workloads. Group grants scale, but membership changes need a fresh token to take effect.
- Manage role assignments as code (Bicep/Terraform), reviewed in PRs. A wrong scope or over-broad role caught in review never becomes a 2am
AuthorizationFailed. - Add a pipeline pre-flight asserting the SPN’s required roles at the workload scope. Fail fast before a release, not mid-release.
- Audit standing privilege regularly.
az role assignment list --allfor Owner/User Access Administrator at high scopes — every one is an escalation path to justify or remove.
Security notes
- Least privilege is the whole point — smallest role, smallest scope.
AuthorizationFailedis RBAC doing its job; grant exactly the missing action, never broaden until it works. An Owner-at-subscription “fix” trades a five-minute incident for a permanent audit finding. - Guard the role-assignment privilege itself.
Microsoft.Authorization/roleAssignments/writeis the keys-to-the-kingdom action — anyone holding it (Owner, User Access Administrator) can grant themselves anything. Keep that set tiny, prefer RBAC Administrator with a condition limiting which roles it can assign. - Use managed identities, not secrets, for workloads. A managed identity with the right data role beats an SPN with a stored secret — no credential to leak, and the object id is auditable in every
AuthorizationFailed. See Managed Identities Demystified: System vs User-Assigned and When to Use Each. - Respect deny assignments — they’re a control, not an obstacle. A managed app’s deny stops consumers tampering with publisher-owned resources; granting around it defeats a deliberate boundary.
- Keep the control/data-plane split intentional. Operators get management roles without data roles (run a resource without reading its contents); apps get data roles without management roles (a compromised app can’t reconfigure infra).
- Prefer Key Vault in RBAC mode and grant data roles narrowly —
Key Vault Secrets Userscoped to the vault or a specific secret keeps secret access uniform with the rest of your RBAC. - Use PIM/just-in-time for high-privilege roles where available. Standing Owner/User Access Administrator is a liability; eligible-then-activate shrinks the window an escalation path is live. Audit with the Activity log.
Cost & sizing
RBAC itself is free — role assignments, definitions, deny-assignment reads, az role assignment list, Check access and az provider operation show cost nothing at any scale. There’s no per-assignment charge and no premium tier gating the model. So “cost” here is the operational and risk cost of getting RBAC wrong.
The expensive failure modes are over-granting and mis-diagnosis. Owner at a subscription to unblock a pipeline costs nothing on the invoice but creates a standing escalation path that an audit flags and a breach exploits — the cost arrives later as remediation. Mis-diagnosing AuthorizationFailed as “needs a bigger role” accretes privilege until no one can answer “who can do what here?” — a multi-week access review. And a CI principal blocked mid-release is revenue and engineer-hours: the ninety-minute Vantix incident was avoidable with one pre-flight check.
There are real service limits worth sizing against, because hitting them surfaces as its own failure (often RoleAssignmentLimitExceeded, not a plain AuthorizationFailed):
| RBAC limit | Approx. ceiling | What hitting it looks like | How to stay under |
|---|---|---|---|
| Role assignments per subscription | ~4,000 (a few thousand) | New assignments rejected; RoleAssignmentLimitExceeded |
Assign to groups, not individuals; clean up stale grants |
| Custom role definitions per tenant | ~5,000 | Can’t create more custom roles | Reuse built-ins; consolidate near-duplicate custom roles |
| Assignment scope depth | MG→sub→RG→resource | Over-deep/odd scopes fail as “invalid” | Assign at RG/sub for most cases |
| Deny assignments | Platform-managed (not user-created) | Hard blocks you can’t grant around | Address the source (managed app/Blueprint) |
The sizing rule is simple: assign to groups, scope to resource groups, reuse built-in roles, and prune. Per-user, per-resource, custom-role-for-everything sprawl marches a subscription toward the assignment ceiling and an unauditable mess. None of it costs rupees directly — it costs the thing more expensive than rupees: the ability to reason about your own access.
Interview & exam questions
1. An AuthorizationFailed names an object id, an action, and a scope. What does each tell you? The object id is the principal ARM evaluated (confirm you’re running as who you think — often an SPN/MI, not you); the action is the exact operation required (which role, which plane); the scope is where a covering assignment must exist. Read all three before running anything — they hand you the hypothesis.
2. A user has Contributor on a resource group but can’t assign a role to a managed identity. Why? Microsoft.Authorization/roleAssignments/write is in Contributor’s NotActions by design, so Contributors can’t escalate. Fix by granting Role Based Access Control Administrator or User Access Administrator at the scope — not Owner.
3. A principal has a granting role at a covering scope yet still gets AuthorizationFailed. Prime suspect? A deny assignment, which overrides any allow — from a managed application, Blueprint, or system protection, never the normal flow. Confirm via IAM → Deny assignments. You can’t grant around it; address the source.
4. Control-plane vs data-plane authorization, with an example? The control plane (ARM) authorises managing a resource via Actions; the data plane (the resource’s own endpoint) authorises operations on the data inside via DataActions. They evaluate separately: Storage Account Contributor manages the account but reads no blobs — you need Storage Blob Data Reader even as the account’s Owner.
5. A role assigned two minutes ago still fails. What’s happening and what do you do? Propagation lag and/or a stale token — live server-side but not yet in caches or the presented token (the error hints “if access was recently granted, refresh”). Wait and re-authenticate (az logout && az login); don’t add a broader role to force it.
6. How do you read a principal’s effective permissions? az role assignment list --assignee <objectId> --all --include-inherited, then check whether any row’s scope is the failing scope or an ancestor, with a role that supplies the action. Portal equivalent: IAM → Check access.
7. NotActions vs a deny assignment? NotActions are operations subtracted from one role’s Actions — another role can add them back. A deny assignment is absolute and overrides every allow at every scope. “This role doesn’t include X” vs “nothing may do X here.”
8. A storage account’s Owner can’t list its blobs. Why, and the fix? Reading blobs is data-plane (DataActions); control-plane Owner/Storage Account Contributor carry none. Grant a data role — Storage Blob Data Reader/Contributor — at the account or container scope.
9. What does “or the scope is invalid” actually mean? Two cases in one string: either the scope is valid and you lack a covering role (permissions — read effective access), or the id is malformed — wrong sub guid, missing segment, deleted resource (scope — fix the id). az resource show --ids tells them apart.
10. Access comes from a group, membership changed today, still fails. Why? Group membership is reflected only in a newly issued token; a cached/long-lived token carries old claims. Force a new token (az logout && az login / restart the runtime). The assignment may be perfect — the token is stale.
11. When choose Role Based Access Control Administrator over User Access Administrator? When a principal (often CI) must assign roles but should be constrained — RBAC Administrator supports a condition limiting which roles it can assign, the least-privilege choice. User Access Administrator can assign any role, broader than most automation needs.
12. A permissions-style failure on a delete, but the role is correct. Non-RBAC causes? A resource lock (CannotDelete/ReadOnly) overrides management ops regardless of role — az lock list. Or an Azure Policy deny rejected the request at admission (Activity log names the policy). Separate subsystems, separate fixes.
These map to AZ-104 (Administrator) — manage Azure identities and governance, RBAC roles, scopes, and assignments — and AZ-500 (Security Engineer) — manage identity and access, least privilege, Key Vault and managed-identity access, and the control/data-plane model. The IaC-grant angle touches AZ-400 / Terraform workflows. A compact cert-mapping for revision:
| Question theme | Primary cert | Exam objective area |
|---|---|---|
| Parse the error; principal/action/scope | AZ-104 | Manage access to Azure resources (RBAC) |
| NotActions, Contributor-can’t-grant | AZ-104 / AZ-500 | Role definitions; least privilege |
| Deny assignments, managed apps | AZ-500 | Implement access control; guardrails |
| Control vs data plane (Storage/Key Vault) | AZ-500 | Secure data and applications; managed identities |
| Propagation/token, group membership | AZ-104 | Manage Entra identities and access |
| Locks vs Policy deny lookalikes | AZ-104 | Implement governance (locks, Policy) |
Quick check
- The error names an object id you don’t recognise, the action
Microsoft.Authorization/roleAssignments/write, and a resource-group scope. What two things does the action immediately tell you? - A principal has Contributor at the subscription and still gets
AuthorizationFailedwriting a resource in a resource group under it. Name two distinct causes and how you’d tell them apart. - True or false: adding
Ownerto a principal will fix a failure caused by a deny assignment. - An app’s Owner can manage a storage account but gets
AuthorizationFailedlisting its blobs. What plane is the failing action on, and what role fixes it? - A role assigned three minutes ago still isn’t working. What’s the most likely cause, and what should you not do?
Answers
- It tells you the call is a grant operation (so you need an access-management role — Owner, User Access Administrator, or RBAC Administrator — not Contributor, whose
NotActionsexcludeMicrosoft.Authorization/*/Write), and that the principal currently lacks that specific action at the named scope. - Either a deny assignment overrides the allow (confirm via IAM → Deny assignments), or the action is one Contributor’s
NotActionsexclude — e.g.roleAssignments/write(confirm viaaz role definition list --name Contributor). Reading the action string distinguishes them: an authorization action points to NotActions; a normal resource action with a granting role points to a deny. - False. A deny assignment overrides every allow, including Owner. The fix is to address the deny’s source (managed app / Blueprint / lock), not to grant a higher role.
- The failing action is on the data plane (
DataActions) — control-plane roles like Owner grant no blob data access. Fix by assigning Storage Blob Data Reader (or Contributor) at the account or container scope. - Propagation lag / a stale token — the assignment is live but not yet effective in caches or the presented token. Wait and re-authenticate (
az logout && az login). Do not add a broader role to “force” it — you’ll leave an over-grant behind.
Glossary
- Azure RBAC — the system deciding whether a principal may perform an action on a resource at a scope, for every control-plane call to ARM.
- Security principal — a user, group, service principal, or managed identity, identified by an Entra object id; the “who” in the error.
- Role definition — a named set of permissions:
Actions,NotActions,DataActions,NotDataActions. - Role assignment — the binding of a principal to a role definition at a scope; the grant that is present or missing.
- Scope — a node in the hierarchy (MG → sub → RG → resource); an assignment covers a target if its scope is the target or an ancestor.
- Action — a single verb on a resource type (e.g.
Microsoft.Storage/storageAccounts/write); what the failing call required. - NotActions — operations subtracted from a role’s
Actions; a narrowing of that role, not an absolute deny. - DataActions / NotDataActions — the data-plane equivalents; operations on the data inside a resource (read a blob, get a secret).
- Deny assignment — an explicit “may NOT perform this action here,” from managed apps/Blueprints/system; overrides all allows, can’t be granted around.
- Control plane / data plane — managing a resource via ARM (
Actions) vs reading/writing its data via its own endpoint (DataActions). - Effective access / Check access — the union of all assignments applying to a principal at a scope, inherited included; what you read to reproduce ARM’s decision.
- Propagation — the time for a new assignment to become effective across caches and tokens (typically minutes).
- Stale token — a bearer token minted before a grant or group change, so it doesn’t carry the new access until refreshed.
- ABAC — a condition attached to a role assignment that further constrains it (by resource tag, blob path, request attributes).
- User Access Administrator — built-in role that can assign any role but not manage resources; the classic “let this principal grant roles.”
- Role Based Access Control Administrator — a constrained access-management role, limitable (via condition) to which roles it may assign; the least-privilege choice for pipelines.
- Resource lock — a
CannotDelete/ReadOnlylock overriding management ops regardless of role; a commonAuthorizationFailedlookalike.
Next steps
You can now read any AuthorizationFailed down to its principal, action and scope and fix it with the narrowest correct grant. Build outward:
- Next: Azure RBAC Fundamentals: Roles, Scopes and Assignments Without the Confusion — solidify the model this playbook reads constantly.
- Related: Build Custom RBAC Roles: Actions, NotActions, and DataActions for Least Privilege — author the exact role when no built-in supplies the failing action.
- Related: Key Vault RBAC vs Access Policies: Choosing and Migrating the Permission Model — the canonical data-plane case behind half these errors.
- Related: Managed Identities Demystified: System vs User-Assigned and When to Use Each — the workload identities that hit AuthorizationFailed most.
- Related: Azure Policy Effects Decoded: Deny vs Audit vs Modify vs DeployIfNotExists — the other “you can’t do that,” and how to tell it from RBAC.
- Related: Terraform AzureRM Provider Auth Failures: 401/403, Stale Tokens & SPN Permission Fixes — the IaC-pipeline cousin of this error.