Background
Sections
IntroductionFoundations1. Resource Hierarchy2. Resource Manager3. Identity and RBAC4. Regions and Availability5. Naming and TaggingVirtual Machines1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetVirtual Network1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetBlob Storage1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure SQL Database1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Kubernetes Service1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Container Registry1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetMicrosoft Entra ID1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure RBAC1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Functions1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAPI Management1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure App Configuration1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Machine Learning1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Monitor1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure AI Foundry1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and Cheatsheet

3. Architecture

11 min read

The machinery most tutorials skip. RBAC looks like a lookup table and behaves like a distributed cache with two independent evaluators, and almost every confusing authorisation failure in Azure comes from one of those two facts rather than from a wrong role.


The authorisation decision, traced end to end

Consider the simplest possible request: a web app's managed identity reading a blob. Two things happen that look like one, and separating them is the point of this page.

Phase 1 — the token.

  1. Code inside the app calls DefaultAzureCredential, which reaches the Instance Metadata Service at a link-local address that isn't routable from outside the host.
  2. IMDS returns a JWT signed by Microsoft Entra ID, scoped to an audience — and the audience matters enormously. A token for https://management.azure.com is not accepted by https://storage.azure.com and vice versa. The SDK requests the right one; hand-rolled code frequently doesn't.
  3. The token carries the principal's oid (object ID), the tenant tid, group membership claims, and an expiry. Role assignments are not in the token. This is the single most important structural fact on the page — the token proves identity, and authorisation is looked up fresh at the resource.

Phase 2 — the decision.

  1. The request goes to https://stapp.blob.core.windows.net/uploads/report.csv with the token as a bearer header. Note the endpoint: this is not ARM.
  2. The Storage service validates the signature against Entra's published key set, extracts the oid, and asks the authorisation system: does this principal hold Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read at this scope?
  3. The evaluator gathers every role assignment applying at or above the blob's scope — the container, the storage account, the resource group, the subscription, every ancestor management group — and unions the dataActions of their role definitions.
  4. If the operation string matches, and any ABAC condition on the matching assignment evaluates true, the read proceeds. Otherwise: HTTP 403.

The corresponding control-plane request — say, reading the account's network rules — goes to management.azure.com instead, and ARM performs step 6 against actions rather than dataActions. Same principal, same scope tree, different evaluator and a different array.

[Image Prompt: 2D minimalistic numbered sequence diagram tracing an authorisation decision, from a workload fetching a token from the instance metadata service, through Microsoft Entra ID signing the token, then splitting into two paths — one to Azure Resource Manager evaluating actions for a control-plane call, one to a service data endpoint evaluating dataActions for a data-plane call — with role assignment lookup shown as a separate step at each evaluator, flat design, clean vector art style, white background]


Control plane vs. data plane — the split, precisely

Azure's split is sharper than AWS's and it catches people constantly. Stated as plainly as possible:

Control plane Data plane
Endpoint management.azure.com stapp.blob.core.windows.net, kv.vault.azure.net, ns.servicebus.windows.net
Evaluator Azure Resource Manager The service itself
Role arrays actions / notActions dataActions / notDataActions
Governs Existence and configuration of the resource The contents
Example operation Rotate the storage account keys Read report.csv
Covered by Owner? Yes No

Owner does not let you read a blob. Nor does Key Vault Contributor let you read a secret, nor Azure Service Bus Contributor let you send a message. Data access requires a role carrying dataActionsStorage Blob Data Reader, Key Vault Secrets User, Azure Service Bus Data Sender.

The diagnostic shortcut: look at the hostname in the error. If it isn't management.azure.com, no amount of Owner will help.

The back door this creates

An Owner or Storage Account Contributor cannot read a blob directly — but they can read the account keys, and a key grants unrestricted data access outside RBAC entirely. So control-plane privilege is a path to data privilege, just an indirect and auditable one.

This is precisely why disabling key-based auth is a real control rather than theatre:

# Make RBAC the only door to the data
az storage account update -g rg-app -n stapp --allow-shared-key-access false

The same shape recurs: Key Vault's enableRbacAuthorization = true retires access policies, AKS's disableLocalAccounts retires the admin kubeconfig, and Service Bus namespaces can have their SAS rules removed. Until you flip these, you have two authorisation systems and only one of them is the one you designed.


The evaluation algorithm

Three properties, and everything surprising follows from them.

1. It is a union. The effective permission set is the union of every role assignment applying at or above the scope. Ten assignments granting Reader and one granting Contributor produce Contributor.

2. There is no author-able deny. notActions subtracts within a single role definition, evaluated before the union. So:

Assignment A: Contributor         (actions: */*, notActions: Microsoft.Authorization/roleAssignments/write)
Assignment B: User Access Administrator (actions: Microsoft.Authorization/*)

Effective: full Contributor AND the ability to write role assignments.

Assignment A's notActions did not prevent anything. It described what A alone grants. This is the mistake that produces a real privilege escalation in an estate that believed it had a boundary.

The one override is a deny assignment, which outranks role assignments and is created only by the platform. You can read them; you cannot write them.

3. Inheritance is downward only and cannot be revoked from below. An assignment at the subscription cannot be narrowed at a resource group. It must be deleted where it was made. In an estate with several years of accreted assignments, this is the reason a "who can delete production" audit is genuinely hard: the answer lives in assignments at four levels, several of them made by people who have left.

Evaluation order in practice

When a request arrives, several independent systems can refuse it, and they produce different error codes. Reading the code first saves the afternoon:

Order System Error code Will a role fix it?
1 Conditional Access (at token issuance, before the request exists) Token never issued; sign-in log has the answer No
2 Deny assignment AuthorizationFailed, but for an Owner No
3 Azure RBAC AuthorizationFailed Yes
4 Azure Policy RequestDisallowedByPolicy No
5 Resource lock ScopeLocked No
6 Resource provider validation / quota Provider-specific, QuotaExceeded No

Only row 3 is an RBAC problem. Rows 4 and 5 are the two most often misdiagnosed as RBAC, and both waste time if you start by granting roles.


Consistency, propagation, and caching — where the latency lives

RBAC is not strongly consistent, and this produces the most common false negative in the whole system: you make the assignment, it's visibly there in the portal, and the request still 403s.

Three separate caches sit between an assignment and its effect:

1. Assignment replication. Role assignment data replicates across regions. A freshly created assignment takes a short while to be visible to every evaluator ⚠️ verify current propagation guidance against current Azure docs. This is why Terraform sometimes needs a wait between creating an assignment and creating a resource that depends on it — covered in Deployment.

2. Token claims. Group membership is carried in the token. Adding a principal to a group has no effect on an already-issued token; it keeps the old claims until it expires. Sign out and back in, or force a refresh. For a managed identity, the platform caches tokens too, so a restart is sometimes the fastest fix.

3. Service-side authorisation caches. Individual services cache decisions briefly.

The operational rule: after a permission change, wait, then force a fresh token, then retest. Debugging a 403 within seconds of making the assignment tells you nothing.

There is a mirror-image problem that matters more for security than convenience: removing a role assignment is also not instant. A revoked grant remains effective for the life of the cached token and the propagation window. For a genuine compromise, deleting the assignment is not enough — you revoke the principal's refresh tokens, or disable the principal outright.


The scope tree at evaluation time

The evaluator walks the ancestry of the target resource ID. For a blob:

container   .../storageAccounts/stapp/blobServices/default/containers/uploads
account     .../storageAccounts/stapp
rg          /subscriptions/{sub}/resourceGroups/rg-app
sub         /subscriptions/{sub}
mg          /providers/Microsoft.Management/managementGroups/mg-platform
mg (root)   /providers/Microsoft.Management/managementGroups/{tenant-id}
root        /

Every level is queried. Two consequences worth internalising:

Management-group assignments are easy to forget and apply everywhere. A Reader at the tenant root management group is a Reader on every resource in every subscription, forever. It's often the right call for a security team, and it's also the answer to "why can this person see production" when nobody assigned them anything in production.

Assigning at the resource group won't cover a resource in a sibling group. Obvious stated plainly, and responsible for a large share of "I assigned it and it didn't work" — the resource was somewhere else. --include-inherited and reading the scope in the error message resolve it in seconds.


Scaling model and limits

RBAC has no throughput to provision, but it has real ceilings, and — this is the Azure-specific part — the scope each is counted at matters more than the number.

Limit Counted at Notes
Role assignments Per subscription The one people actually hit. Group-based assignment is the mitigation ⚠️ verify current limit against current Azure docs
Role assignments Per management group Separate, smaller ceiling ⚠️ verify
Custom role definitions Per tenant Not per subscription. Shared across everything ⚠️ verify
Groups in a token Per token Beyond a threshold, Entra emits a group overage claim instead of the group list, and the app must query Graph. Breaks naive apps ⚠️ verify current threshold
ABAC conditions Per assignment One condition per assignment; length limits apply ⚠️ verify

The group-overage claim deserves its own line because the failure is so indirect: a user in very many groups gets a token without their group list, and an application that reads groups from the token concludes they're in none. Azure's own evaluators handle it; your code might not.

ARM control-plane throttling applies to reading and writing assignments like any other ARM operation: 429 with a Retry-After header. A pipeline that enumerates assignments across hundreds of resource groups will hit it. Use Azure Resource Graph for estate-wide queries instead of looping az role assignment list — it's the query engine built for exactly this and it doesn't consume the same budget.


Failure modes worth recognising

403 immediately after granting. Propagation or a cached token. Wait, refresh, retest.

403 on the data plane while holding Owner. Wrong plane. Look at the hostname. You need a dataActions role.

403 for a wrong-audience token. Hand-rolled token acquisition asking for https://management.azure.com and calling vault.azure.net. The signature validates and the audience check fails; the error can look like a permission problem.

Worked yesterday, fails today, nothing changed. A system-assigned managed identity whose resource was replaced. New object ID, orphaned assignment, runtime-only failure. Check whether the assignment's principal still resolves.

ScopeLocked. A CanNotDelete or ReadOnly lock, quite possibly inherited from the resource group. Not RBAC.

RequestDisallowedByPolicy. Azure Policy. Different system, different owner, no role will help.

An Owner is refused. Deny assignment, or a lock. Check both before assuming a platform bug.

Terraform 403s on an assignment it created moments ago. The classic ordering problem: the assignment exists but hasn't propagated to the evaluator the next resource talks to.

Subscription moved between tenants and everything broke. Every role assignment and managed identity referenced principals in the old directory. They're all orphaned. Nobody expects this the first time and there is no quick fix — the assignments have to be recreated.


Reliability posture

RBAC's availability is ARM's availability; there is no separate SLA ⚠️ verify current terms against the current SLA documentation. Two practical implications:

Authorisation data is replicated and read-optimised, so evaluation survives conditions that would stop you from changing assignments. In a control-plane incident you can often still use resources you already have access to while being unable to grant new access.

That makes break-glass a real design requirement. If your only path to Owner is PIM activation and the thing that's broken is the activation path, you need a pre-existing, credential-managed, alarmed-on-use emergency account. Production covers what that looks like.


Next: Getting Started →

← Back to the Azure RBAC overview · ← Previous: Core Concepts