LowCodeStacks™
Sign in

Azure 403 after assigning a role: control plane vs data plane, scopes and propagation

You assigned the role and Azure still says 403. Nearly always one of four things: the role is control plane and the call is data plane, the scope is wrong, the assignment hasn't propagated yet, or a firewall or private endpoint is answering before RBAC does. The data-plane role per service, the waiting times, and how to tell which one you have.

Identity & securityFix a problem9 min read

Checked against Microsoft Learn · 8 sourcesPosted by the Maker DeskHow we write guides

"I gave them Contributor and they still get 403" is the most common Azure access question there is, and the role assignment is usually fine. The call is being refused for a reason the role can't fix: the role manages the resource but the call reads its data; the role is at the wrong scope; the assignment exists but hasn't reached the service yet; or the request never got as far as an authorisation check because a firewall or private endpoint turned it away. This guide tells the four apart, gives the data-plane role each service wants, and sets out the waits that make people think a fix didn't work.

Read the error first

The message says which cause you have, if you read past "403":

The message saysCause
AuthorizationFailed from Resource Manager: "does not have authorization to perform action … over scope …"A control-plane role is missing at that scope (cause 2)
AuthorizationPermissionMismatch (Storage), Caller is not authorized to perform action on resource … Assignment: (not found) (Key Vault), "does not have required RBAC permissions to perform action [Microsoft.DocumentDB/…/readMetadata]" (Cosmos DB)A data-plane role is missing (cause 1), or it's there and not propagated yet (cause 3)
AuthorizationFailure "This request is not authorized to perform this operation" (Storage), "Client address … is not authorized and caller is not a trusted service", "Public access is disabled and the request was not made from a trusted service or via an approved private link" (Key Vault), "This TCP connection does not allow access to vault.azure.net"The network refused it (cause 4)
KeyBasedAuthenticationNotPermittedShared-key access is off and the client is using an account key; switch it to Entra ID

Cause 1: the role is control plane and the call is data plane

Azure roles have two lists. Actions cover managing a resource through Resource Manager; DataActions cover using it through its own endpoint. Owner and Contributor have * in Actions and nothing in DataActions: they can create, configure and delete a storage account, and can't read a blob in it. That separation is deliberate, so a wildcard role can't read every byte in a subscription. The fix is the data-plane role for the service, at the resource or resource-group scope:

To do thisAssign
Read blobsStorage Blob Data Reader
Read and write blobsStorage Blob Data Contributor (overwriting needs delete too, which it has)
Manage containers and POSIX ACLs as wellStorage Blob Data Owner
Queues and tablesStorage Queue Data Contributor, Storage Table Data Contributor (and the Reader variants)
Read secrets in a vault that uses RBACKey Vault Secrets User
Create and manage secretsKey Vault Secrets Officer; Key Vault Administrator for everything in the vault
Only list what a vault holds, not the valuesKey Vault Reader
Query Cosmos DB (NoSQL, Table)Cosmos DB Built-in Data Reader or Data Contributor (account scope; a Cosmos role, not an Azure one)
Send or receive on Service BusAzure Service Bus Data Sender / Receiver
Call a Foundry model or agentFoundry User

Three traps around this:

  • Key Vault has two permission models. A vault on Azure RBAC (the default for new vaults since API version 2026-02-01) ignores access policies, and the Azure roles above apply. A vault on the legacy access policy model ignores Azure data roles, and the identity must be in the vault's Access policies. Switching a vault to RBAC invalidates every access policy at once; assign the roles first. Note that Key Vault Contributor is control plane only.
  • Account keys are the loophole. Any control-plane role that includes Microsoft.Storage/storageAccounts/listKeys/action (Contributor does) can fetch the storage keys and read data with Shared Key authorisation, bypassing RBAC. That's why "I can read it in the portal" doesn't prove the role is right: the portal may be using the key. Disable shared-key access, and test with the identity that will do the work.
  • Databricks workspaces. Storage data roles assigned at the subscription scope don't apply to a storage account inside a Databricks-managed resource group; assign at the resource group, account or container.

Cause 2: the wrong scope, or the wrong principal

Assignments inherit down the hierarchy: management group → subscription → resource group → resource. A role on resource group A gives nothing in resource group B, and a role on one storage account gives nothing on its neighbour. Check with az role assignment list --assignee <object id> --all -o table, and read the Scope column, not just the role name.

The principal has to be the one making the call:

  • Which identity is it, really? DefaultAzureCredential may be using your az login account locally and the app's managed identity in Azure, or a user-assigned identity you forgot to name. Turn on the SDK's logging to see which credential produced the token.
  • Same name, different object. Display names aren't unique. Assign by object ID (--assignee-object-id, with --assignee-principal-type), which also avoids the PrincipalNotFound replication error when you assign a role to an identity you created seconds ago.
  • A new system-assigned identity after a redeploy. Deleting and recreating a resource creates a new principal; the old role assignment is now Identity not found, and a Bicep role assignment with the same name fails with RoleAssignmentUpdateNotPermitted. Name role assignments with guid(scope, principalId, roleDefinitionId) so they change when the principal does.
  • A moved resource. Moving a resource to another resource group or subscription orphans role assignments made directly on it; recreate them.
  • A guest, or a conditional assignment. Guests need an explicit assignment, and an assignment with a condition (for example "only these roles, only to users") can refuse what the role name promises.

Cause 3: it has not propagated yet

What changedWait
A role assigned or removedUp to 10 minutes. Signing out and in, or refreshing the access token, picks it up sooner. Storage's own guidance says up to 30 minutes in some cases
A role with data actions assigned at management-group scopeData-plane access can take several hours to update
A managed identity added to a group, or the group's role changedThe identity's token is cached per resource for around 24 hours, and can't be refreshed early. Assign roles directly to a user-assigned identity instead of through groups when you need changes to apply quickly
A federated credential createdA few minutes; token requests just after creation can fail with AADSTS70021
Key Vault role assignedMicrosoft says one to five minutes; a Forbidden straight after assigning usually means not yet
A storage firewall rule or service endpoint policyUp to 5 minutes for IP rules; up to 15 for service endpoint policies

A Forbidden in the first minutes after a correct assignment is the service telling the truth about now. Add a retry with backoff to anything that assigns a role and uses it in the same run.

Cause 4: the network answered before RBAC did

A storage account or vault with public network access disabled, a firewall allow-list, or a private endpoint refuses requests that don't arrive the right way, and it says 403, not "connection refused". Signs it's the network: the message mentions a client address, trusted services, public access or a TCP connection; access works from one place and not another; it broke after a network or DNS change.

The usual culprit with a private endpoint is DNS: the client resolves myaccount.blob.core.windows.net to the public IP, so the request arrives on the public endpoint and the firewall rejects it. From the client, nslookup the name; a 10.x or 172.x private address means the private endpoint is in use, a public address means it isn't, and the fix is linking the private DNS zone to the virtual network, or forwarding the privatelink zone from your own DNS servers. Two more:

  • A second host name. Data Lake (dfs), queues and tables have their own endpoints; a private endpoint for blobs doesn't cover them.
  • Who's outside the network. The portal, a hosted pipeline agent and your laptop are outside the virtual network. Allow the trusted-services bypass, add the client IP temporarily, or run the agent inside the network.

Once the network is right and the 403 remains, it's cause 1 or 3 after all. Microsoft's private-endpoint troubleshooter runs the checks in that order: DNS, connection state, firewall, then the data-plane role.

Prove it before you close the ticket

bash
# Who am I, and what does Azure think I hold?
az ad signed-in-user show --query id -o tsv
az role assignment list --assignee <object id> --all -o table

# Try the data plane as that identity, with Entra auth (not a key)
az storage blob list --account-name <account> --container-name <container> --auth-mode login -o table
az keyvault secret list --vault-name <vault> -o table

Run the same commands as the service's own identity where you can (az login --identity on a VM, or the app's logs), because the question is never whether you can read the data; it's whether the caller can.

Sources

Did this fix it?

Comments

No comments yet. Share what worked for you.

Sign in to comment. Comments show under a display name you choose, never your email.