Fix "access denied" on Azure, layer by layer
The triage for any Azure refusal — 401, 403, AuthorizationFailed, ForbiddenByRbac, RequestDisallowedByPolicy, AADSTS — in the order that finds the cause fastest: was a token issued, which identity and tenant does it carry, which plane refused (Resource Manager, a service's data plane, Microsoft Graph), is there a role at the right scope, is a deny assignment, policy, firewall or service permission model in the way, or has the change just not applied yet.
markdownguidev15KKJ-HPJRstage: operatetrust: officialservice: entraservice: rbacservice: policyinterface: anyAzure access denied403 Forbidden Azure401 Unauthorized AzureAuthorizationFailedForbiddenByRbacRequestDisallowedByPolicywhich identity is callingdecode access token oid tid audaz account showaz account get-access-tokencontrol plane vs data planedata role missingdeny assignmentAzure Policy denyservice firewall 403InvalidAuthenticationTokenTenantwrong tenantrole assignment not applied yetMicrosoft Graph Authorization_RequestDenied
Work outward from the token. Most wasted time comes from adding roles when the call was made by a different identity, in a different tenant, or refused by something that isn't RBAC at all. Each step below either finds the cause or rules a layer out.
1. Was a token issued at all?
An AADSTS… code, a CredentialUnavailableError or AggregateAuthenticationError means no token: nothing about roles matters yet.
- AADSTS for an app or service principal →
reference/entra/errors.md - AADSTS for a user (MFA, conditional access) →
reference/entra/sign-in-errors.md - Federated (GitHub, Kubernetes) →
reference/entra/federated-credential-errors.md - SDK credential messages →
reference/azure-identity/errors.md
2. Which identity, which tenant?
az account show --query "{user:user.name, type:user.type, tenant:tenantId, sub:id}"
az account get-access-token --scope https://management.azure.com/.default --query accessToken -o tsv \
| node -e "const p=require('fs').readFileSync(0,'utf8').split('.')[1]; const c=JSON.parse(Buffer.from(p,'base64url')); console.log({oid:c.oid, tid:c.tid, aud:c.aud, appid:c.appid ?? c.azp, roles:c.roles, scp:c.scp})"
In code, log the authenticated account (reference/azure-identity/credential-chain.md). Compare:
oidwith the principal in your role assignment. A differentoidmeansDefaultAzureCredentialpicked another credential, or a user-assigned identity wasn't selected.tidwith the resource's tenant.InvalidAuthenticationTokenTenantmeans the token is from another tenant: sign in with--tenant <id>.audwith the service. A Resource Manager token can't call Key Vault's data plane, and vice versa.
The command prints the token's claims, not the token. Never paste a full token into a chat or a log.
3. Which plane refused?
| You called | The refusal looks like | Permission comes from |
|---|---|---|
Resource Manager (management.azure.com: create, configure, delete, az resource commands, Bicep) | AuthorizationFailed naming an action like Microsoft.Storage/storageAccounts/write | Azure RBAC Actions |
A service's data endpoint (*.vault.azure.net, *.blob.core.windows.net, *.servicebus.windows.net…) | 403 with a service code (ForbiddenByRbac, …) | Azure RBAC DataActions, or the service's own model (Key Vault access policies, shared keys, SAS) |
| Microsoft Graph (create apps, read users) | Authorization_RequestDenied / Insufficient privileges to complete the operation | Microsoft Entra roles or Graph application permissions with admin consent, not Azure RBAC |
Owner and Contributor carry no data actions. "I'm Owner and still get 403 reading a secret" is the expected result: assign the data role.
4. Is there a role at the right scope?
az role assignment list --assignee <oid> --all --include-inherited --include-groups \
--query "[].{role:roleDefinitionName, scope:scope}" -o table
The scope must be the resource or a parent of it. If the role is there but new, wait up to about 10 minutes and get a fresh token. For a managed identity whose access comes through an Entra group, expect hours (reference/managed-identity/errors.md). Errors from creating the assignment itself: reference/rbac/errors.md.
5. Something other than RBAC
| Layer | Signal | Check |
|---|---|---|
| Deny assignment (deployment stack deny settings, managed apps) | 403 despite a matching role | az rest --method get --uri "https://management.azure.com/<scope>/providers/Microsoft.Authorization/denyAssignments?api-version=2022-04-01" |
| Azure Policy | RequestDisallowedByPolicy, or a deny/denyAction 403 naming a policy | reference/policy/effects-and-errors.md |
| Resource lock | Writes or deletes refused although the role allows them. ReadOnly acts like Reader; CanNotDelete blocks deletion only | az lock list --resource-group <rg> |
| Service firewall / private endpoint | 403 naming the client IP (Client address … is not authorized …) | The service's network rules (reference/keyvault/access-and-errors.md; storage and networking families) |
| Service permission model | Key Vault in access-policy mode ignores RBAC data roles; storage accounts can refuse shared-key or SAS auth | az keyvault show -n <kv> --query properties.enableRbacAuthorization |
6. Still refused
Re-run the exact call with --debug (CLI) or SDK logging at info, keep the response's correlation or request ID, and compare the action named in the error with the role definition's Actions / DataActions (az role definition list --name "<role>" --query "[0].permissions"). A custom role missing one action is a common last cause.