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

7. Production

14 min read

The difference between "I granted the role and it worked" and "I run the access model for an estate". RBAC is unusual among Azure topics here: it costs nothing to run, so the production concerns are almost entirely about governance decay — an access model is correct on the day it's designed and wrong eighteen months later unless something actively holds it in place.

[Image Prompt: 2D minimalistic diagram of five production pillars — security, cost, scaling and limits, observability, and reliability — arranged around a central Azure RBAC role assignment object, flat design, clean vector art style, white background]


Security

RBAC is a security control, so this section is about securing the control itself.

Least privilege, expressed as decisions

Least privilege is easy to agree with and hard to operationalise. Four concrete decisions carry most of the value:

1. Groups, not people. Every standing assignment targets an Entra group. The exception is PIM-eligible assignments, where a person's name on the activation record is the point. This turns access review into membership review and keeps you well under the assignment ceiling.

2. Narrowest scope that works, defaulting to the resource group. Sub-resource scopes — a single blob container, a single queue, a single secret — are underused and dramatically narrower than the parent. Reach for them for workload identities, where the required access is precisely known.

3. Service-specific Contributor over Contributor. Contributor can delete every resource in scope. Website Contributor + Storage Blob Data Contributor usually covers the same job and is about ten minutes more work. The reasoning "they're not Owner, so it's contained" misreads what containment means.

4. No standing privileged access. Where the licence allows it, Owner and User Access Administrator are PIM-eligible, never assigned. Where it doesn't, they're held by a small named group with an alert on every use.

The permissions that are escalation paths

Four grants are qualitatively different from the rest, because holding any of them means you can grant yourself everything else. Treat them as a separate category with separate review:

Permission Held by Why it's terminal
Microsoft.Authorization/roleAssignments/write Owner, User Access Administrator, RBAC Administrator Grant yourself anything, anywhere in scope
Microsoft.Authorization/roleDefinitions/write Owner, User Access Administrator Author a custom role with * and assign it
Microsoft.Authorization/elevateAccess/action Global Administrators, on request User Access Administrator at root scope / — the whole tenant
Reading a resource's keys (listKeys/action) Most service Contributor roles Bypasses data-plane RBAC entirely

The fourth is the subtle one and worth restating: Storage Account Contributor is effectively Storage Blob Data Owner as long as shared-key access is enabled, because it can read the account keys. The same shape applies to Service Bus SAS rules, Event Hubs, and Cosmos DB keys. Your data-plane role design is only load-bearing once the key path is closed.

Kill the parallel authorisation paths

The single highest-value hardening pass in an Azure estate, and it's free:

# Storage: RBAC becomes the only door to blob/queue/table data
az storage account update -g rg-app -n stapp --allow-shared-key-access false

# Key Vault: retire access policies
az keyvault update -g rg-app -n kv-app --enable-rbac-authorization true

# AKS: retire the admin kubeconfig that bypasses every Azure role assignment
az aks update -g rg-app -n aks-app --disable-local-accounts

# Azure SQL: retire SQL logins
az sql server ad-only-auth enable -g rg-app -n sql-app

Enforce them with Azure Policy rather than a runbook — there are built-in definitions covering several of these ⚠️ verify the current built-in set against current Azure docs. A convention says "we don't enable shared keys"; a policy makes it fail.

Managed identity, and the trap that bites in production

Use managed identities for everything inside Azure and workload identity federation for everything outside it. No client secrets, no connection strings, no publish profiles.

The production-specific trap: a system-assigned identity gets a new object ID whenever its resource is replaced. The role assignment then references a dead principal, terraform apply still succeeds, the pipeline goes green, and the application 403s at runtime. Use user-assigned identities for anything that gets recreated, which in practice means anything managed by IaC.

Break-glass accounts

If your only path to Owner is PIM activation, and the thing that's broken is the activation path, you have no path. Two emergency accounts, and the details matter:

  • Cloud-only, excluded from Conditional Access policies that could lock them out, with credentials split across two people or held in a physical safe.
  • Standing Owner at the root management group — the one place standing privilege is correct.
  • Alerting on any sign-in, wired to a pager rather than an inbox. The account existing is fine; the account being used silently is not.
  • Tested twice a year. An untested break-glass account is a documented assumption.

Guest and cross-tenant access

Guest users are legitimate principals and can hold any Azure role. Two things worth checking regularly: that guests aren't holding privileged roles (a partner's engineer with Contributor on production is a supply-chain risk you've accepted implicitly), and that subscription tenant moves haven't orphaned everything. Moving a subscription between tenants invalidates every role assignment and managed identity in it, because those principals lived in the old directory. Nobody expects this the first time and there is no quick fix.


Cost

Azure RBAC is free. No meter, no SKU, no per-assignment charge. What costs money is the governance layer around it, and the honest framing is what you're buying rather than what you're paying:

Feature Licence What it buys
Azure RBAC, custom roles, ABAC conditions Free Everything on this page except the rows below
PIM — just-in-time activation, approval, expiry Entra ID P2 or Entra ID Governance Turns "who is an Owner" from a standing list into an activation log
Access reviews P2 / Governance Recurring attestation; the mechanism that stops decay
Conditional Access on the Azure management plane P1 Require MFA or a compliant device before a token for ARM is issued at all
Identity Protection risk signals P2 Risk-based blocking before authorisation is reached

⚠️ verify current feature-to-licence mapping and pricing against current Azure docs — Microsoft moves features between tiers.

The cost trap is a licensing one, not a billing one. Entra licences are per user who benefits from the feature, not per tenant. Nothing blocks you technically from applying a P1-gated Conditional Access policy to unlicensed users; you find out at audit. If PIM covers thirty privileged people, licence thirty — not the whole organisation, and not zero.

The optimisation worth naming: PIM is usually far cheaper than people assume, because the population that needs privileged access is small. The comparison to make is not "P2 for everyone" but "P2 for the twenty people who currently hold standing Owner" — against the cost of one incident caused by one of them.

What keeps costing when nothing is happening: nothing, for RBAC itself. The Log Analytics workspace holding your activity log does have a real ingestion and retention bill, and administrative logs are low-volume enough that it's rarely the line item anyone notices. Set a retention period deliberately anyway — audit requirements are usually longer than the default.


Scaling and limits

RBAC has no throughput to provision, but it has ceilings, and the scope each is counted at matters more than the number. A limit without a scope is useless in Azure.

Limit Counted at Practical note
Role assignments Per subscription The one estates actually hit. Group-based assignment is the mitigation and it's usually a 10–50× reduction ⚠️ verify current limit
Role assignments Per management group A separate, smaller ceiling ⚠️ verify
Custom role definitions Per tenant Not per subscription — shared across the whole estate ⚠️ verify
Role assignments per PIM eligible schedule Per scope ⚠️ verify
Groups in a token before overage Per token Beyond the threshold, Entra emits a group overage claim instead of the list ⚠️ verify current threshold
ABAC condition length Per assignment One condition per assignment ⚠️ verify

Hard vs. soft: the assignment ceilings have historically been raisable in some cases via support request ⚠️ verify current behaviour against current Azure docs. Treat them as hard when designing — an architecture that needs the limit raised is an architecture that should have used groups.

The group overage claim deserves attention because the failure is so indirect. A user in very many groups receives a token without their group list; Azure's own evaluators handle it by querying Graph, but application code that reads groups from the token concludes the user is in none. The symptom is one particular power user losing access to your app while RBAC works fine.

Estate-wide queries: use Azure Resource Graph, not a loop. Enumerating assignments across hundreds of resource groups with az role assignment list will hit ARM throttling (429 with Retry-After). Resource Graph is the query engine built for this and doesn't consume the same budget:

authorizationresources
| where type =~ "microsoft.authorization/roleassignments"
| extend principalId = tostring(properties.principalId),
         roleDefId   = tostring(properties.roleDefinitionId),
         scope       = tostring(properties.scope)
| summarize assignments = count() by subscriptionId
| order by assignments desc

That query is also your early warning on the per-subscription ceiling.


Observability

Nothing about RBAC is logged usefully by default. The activity log captures assignment changes but is retained only briefly unless routed somewhere. This is the diagnostic setting to create on day one of a subscription's life.

az monitor diagnostic-settings subscription create \
  --name activity-to-law \
  --location uksouth \
  --workspace "$(az monitor log-analytics workspace show -g rg-platform-obs -n law-platform --query id -o tsv)" \
  --logs '[{"category":"Administrative","enabled":true},{"category":"Security","enabled":true},{"category":"Policy","enabled":true}]'

The log categories that matter

Source Table Answers
Activity log, Administrative AzureActivity Who created, changed, or deleted a role assignment or definition
Entra sign-in logs SigninLogs Which identity actually authenticated, and whether Conditional Access intervened
Entra audit logs AuditLogs Directory-role changes, group membership changes, PIM activations
Entra PIM AuditLogs / PIM reports Who activated what, when, with what justification

Note that group membership changes are in AuditLogs, not AzureActivity. If you assign to groups — as you should — then the log that answers "who gained access to production" is the directory audit log, not the Azure activity log. An estate that monitors only the latter has a blind spot exactly where its access model lives.

The KQL query worth keeping

The most common question about RBAC in production is "who granted this, and when":

AzureActivity
| where OperationNameValue in~ (
    "MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE",
    "MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/DELETE",
    "MICROSOFT.AUTHORIZATION/ROLEDEFINITIONS/WRITE",
    "MICROSOFT.AUTHORIZATION/ELEVATEACCESS/ACTION")
| where ActivityStatusValue == "Success"
| extend props = parse_json(Properties)
| extend body  = parse_json(tostring(props.requestbody))
| project TimeGenerated,
          op        = OperationNameValue,
          Caller,
          CallerIpAddress,
          scope     = tostring(props.entity),
          roleId    = tostring(body.Properties.RoleDefinitionId),
          principal = tostring(body.Properties.PrincipalId)
| order by TimeGenerated desc

The three alerts to configure

1. Privileged role assigned. Owner or User Access Administrator written at subscription or management-group scope. Should notify the security channel within minutes.

2. Elevate access used. MICROSOFT.AUTHORIZATION/ELEVATEACCESS/ACTION, unconditionally, to a pager. A Global Administrator granting themselves root-scope User Access Administrator is legitimate as break-glass and must never be silent.

3. Break-glass account sign-in. From SigninLogs, any authentication by the emergency accounts.

4. Custom role definition changed. Lower urgency, but a custom role gaining a wildcard is a silent widening of everything assigned it.

Baseline hygiene queries

Run these monthly; each has produced findings in every estate I'd expect you to inherit.

# Standing privileged assignments anywhere
az role assignment list --all \
  --query "[?roleDefinitionName=='Owner' || roleDefinitionName=='User Access Administrator'].{role:roleDefinitionName, principal:principalName, type:principalType, scope:scope}" \
  -o table

# Orphaned assignments — principal no longer resolves
az role assignment list --all \
  --query "[?principalName==null].{role:roleDefinitionName, oid:principalId, scope:scope}" -o table

# Direct-to-user assignments, which should be rare
az role assignment list --all --query "[?principalType=='User'].{principal:principalName, role:roleDefinitionName, scope:scope}" -o table

# Custom roles, and whether any contains a wildcard
az role definition list --custom-role-only true \
  --query "[].{name:roleName, actions:permissions[0].actions}" -o json

# Guests holding any role
az role assignment list --all --query "[?contains(to_string(principalName), '#EXT#')]" -o table

The orphaned assignments query is the one people are most surprised by. They accumulate silently from every deleted service principal and every replaced system-assigned identity, they count against the subscription ceiling, and they make an access audit unreadable.

Access reviews as the anti-decay mechanism

Queries detect. Access reviews correct. A recurring review that asks each group's owner "does everyone here still need this" is the only mechanism that reliably removes access, because nobody ever files a ticket to lose a permission. Requires P2/Governance licensing ⚠️ verify. Without it, the manual equivalent is a quarterly calendar entry and the discipline to act on it — which is worse but not nothing.


Reliability

RBAC's availability is ARM's availability; there's no separate SLA ⚠️ verify current terms against the current SLA documentation. Three properties shape how you plan around it.

Authorisation data is replicated and read-optimised. Evaluation survives conditions that would prevent you from changing assignments. In a control-plane incident you can typically keep using access you already hold while being unable to grant new access — which is exactly the wrong way round during an incident, and the reason break-glass accounts must pre-exist rather than be created on demand.

Propagation is eventually consistent, in both directions. A new assignment takes time to become effective ⚠️ verify current guidance. So does a removal — a revoked grant remains usable for the life of the cached token plus the propagation window. For a genuine compromise, deleting the assignment is insufficient: revoke the principal's refresh tokens or disable the principal.

# Actually cut off a compromised user, in order
az ad user update --id user@contoso.com --account-enabled false
az rest --method POST \
  --url "https://graph.microsoft.com/v1.0/users/user@contoso.com/revokeSignInSessions"

Regional outages don't stop authorisation but can stop the resources being authorised. RBAC is not in the failure path you need to design around; the resources are.

The failure drill worth rehearsing

Twice a year, with someone who hasn't done it before:

  1. Break-glass activation. Sign in with an emergency account, confirm root-scope access, confirm the alert fired. This is the one that's always broken when it's finally needed.
  2. Revocation timing. Remove a role assignment from a test identity and measure how long it keeps working. Everyone's mental model of this is wrong until they've measured it.
  3. Orphaned-identity recovery. Force-replace a resource with a system-assigned identity and observe the runtime 403 that terraform apply reported as success. The lesson lands better than reading it.
  4. Reconstruct access from code. Delete a resource group's assignments and restore them with terraform apply. If that doesn't fully restore access, you have undeclared grants — which is the finding, not the failure.

Number 4 is the drill that validates the whole "access is code" premise. Most estates fail it the first time.


The production checklist

  • Every standing assignment targets a group, not a person
  • Owner and User Access Administrator are PIM-eligible, not assigned
  • Two break-glass accounts exist, are excluded from lockout policies, alert on sign-in, and were tested this year
  • Shared keys, Key Vault access policies, AKS local accounts, and SQL logins are disabled, enforced by Policy
  • Workloads use user-assigned managed identities; pipelines use workload identity federation
  • The activity log is routed to a Log Analytics workspace with deliberate retention
  • Alerts exist for privileged assignment, elevate access, and break-glass sign-in
  • Every managed assignment sets description, so undeclared grants are queryable
  • Nightly terraform plan for drift, plus a Resource Graph query for assignments outside state
  • Access reviews recurring on every group holding a privileged role
  • Orphaned assignments swept monthly
  • Custom roles enumerate actions — no wildcards
  • Assignment counts per subscription tracked against the ceiling

Next: Interview Questions →

← Back to the Azure RBAC overview · ← Previous: Integrations