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

2. Core Concepts

16 min read

Every noun you'll meet in the Access control (IAM) blade, defined once. The format throughout is term → plain-English analogy → precise technical definition, because the vocabulary here is unusually treacherous: several pairs of terms sound like synonyms and aren't.

[Image Prompt: 2D minimalistic labelled diagram of the Azure RBAC object model, showing a role assignment at the centre with three arrows pointing outward to a security principal, a role definition containing four permission arrays, and a scope in the resource hierarchy, flat design, clean vector art style, white background]


The role assignment — the only grant there is

Analogy: a programmed card reader on a door. It names a badge group, a permitted action, and the door it guards. Remove the reader's rule and the badge stops working.

Technically: a role assignment is an ARM resource of type Microsoft.Authorization/roleAssignments, whose name is a GUID and whose properties are three references:

role assignment = principal (who) + role definition (what) + scope (where)

There is no fourth field that matters and no policy document anywhere. The assignment object is the grant. Its full resource ID looks like this, and this shape is worth recognising because it appears in every Terraform plan and every activity-log entry:

/subscriptions/{sub}/resourceGroups/rg-app/providers/Microsoft.Authorization/roleAssignments/{guid}

The GUID being the name rather than a generated suffix has one consequence that surfaces immediately in IaC: because ARM is idempotent on resource name, a deterministic GUID makes an assignment re-appliable, and a random one creates a duplicate. Bicep solves this with the guid() function; Terraform generates one for you unless you set name explicitly. Covered in Deployment.

Term Analogy Technical definition
Role assignment The programmed rule on one card reader A Microsoft.Authorization/roleAssignments resource binding one principal to one role definition at one scope
Role definition The job description the badge grants A named collection of four permission arrays plus assignableScopes; either built-in (Microsoft-managed) or custom (yours)
Scope The door and everything behind it A resource ID at management-group, subscription, resource-group, resource, or sub-resource level; grants inherit downward from it
Security principal The badge holder A user, group, service principal, or managed identity — identified by an Entra object ID

Security principals — the four things that can hold a role

A security principal is anything assignable. There are four, and choosing the wrong one is a design smell rather than an error.

Principal What it is Use it for Object ID comes from
User A human identity in the tenant, member or guest People — but rarely as a direct assignment target az ad user show
Group A collection of users and/or service principals The default target. Membership changes without touching Azure az ad group show
Service principal The identity of an application in this tenant Automation and tools outside Azure az ad sp show
Managed identity A service principal Azure creates and whose credentials Azure rotates Anything running inside Azure. Almost always the right answer az webapp identity show, az identity show

Assign to groups, not users. Two reasons, both practical. There is a ceiling on role assignments per subscription ⚠️ verify the current limit against current Azure docs, and it is reached by real organisations. And an access review over thousands of individual assignments is archaeology, whereas a membership review is a question the business can actually answer.

The one exception worth naming: PIM-eligible assignments to individuals are legitimate, because the point there is the activation record, which needs a person's name on it.

Managed identity — system-assigned vs. user-assigned

The full treatment is in Identity and RBAC; the RBAC-relevant part is the lifecycle difference, because it determines whether your role assignments survive a redeploy.

System-assigned User-assigned
Lifecycle Created and destroyed with the resource Its own ARM resource (Microsoft.ManagedIdentity/userAssignedIdentities)
Object ID stability New object ID every time the resource is replaced Stable across every consumer's lifecycle
Sharing One resource, one identity Many resources share one identity, so grant the role once
The RBAC trap Any Terraform change that forces replacement gives a new principal; existing assignments now point at a dead object ID and fail at runtime, not at apply time Nothing stops it outliving its consumers — orphaned identities with live role assignments accumulate

If a resource is recreated with any regularity, use a user-assigned identity. The failure mode of the system-assigned trap is particularly nasty: terraform apply succeeds, the pipeline goes green, and the application 403s in production.

Orphaned assignments

Delete a principal and its assignments remain, pointing at an object ID that no longer resolves. The portal shows them as "Identity not found". They're harmless functionally and terrible for audit — and they count against the assignment limit. az role assignment list --all with a filter for unresolvable assignees is the cleanup query; Production has it.


The role definition — four arrays and one list

Analogy: a job description. It lists what the job may do, what it explicitly isn't part of, and which departments may hire for it.

Technically: a JSON object with a name, a description, assignableScopes, and a permissions array whose single element contains four string arrays:

{
  "roleName": "Storage Blob Data Reader",
  "description": "Allows for read access to Azure Storage blob containers and data",
  "assignableScopes": ["/"],
  "permissions": [{
    "actions": [
      "Microsoft.Storage/storageAccounts/blobServices/containers/read",
      "Microsoft.Storage/storageAccounts/blobServices/generateUserDelegationKey/action"
    ],
    "notActions": [],
    "dataActions": [
      "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read"
    ],
    "notDataActions": []
  }]
}

The split between the two pairs of arrays is the control-plane / data-plane line, made literal:

  • actions — control-plane operations, evaluated by ARM. Creating, reading the configuration of, updating, and deleting the resource.
  • dataActions — data-plane operations, evaluated by the service's own endpoint. Reading a blob, getting a secret, sending a message.
  • notActions / notDataActionssubtractions from this role only. Not denials. Another assignment granting the same action still wins.

That last point earns its own paragraph. notActions is the most misunderstood field in Azure RBAC. It exists so Microsoft can define Contributor as "everything, minus the ability to write role assignments" without enumerating every action in Azure. It is arithmetic inside one role definition, evaluated before the union across assignments. It is not a security boundary.

Operation strings and wildcards

Operation strings are {Provider}/{resourceType}/{operation}, where the operation is read, write, delete, or a named action. A trailing * wildcards the rest:

Microsoft.Compute/virtualMachines/read
Microsoft.Compute/virtualMachines/restart/action
Microsoft.Storage/*                                 ← everything in the Storage provider
*/read                                              ← the Reader role, essentially

To find the exact string for something, ask the provider rather than guessing:

az provider operation show -n Microsoft.Storage --query "resourceTypes[?name=='storageAccounts'].operations[].name" -o tsv

* in actions is what makes Contributor and Owner broad. When writing a custom role, avoid it; a wildcard silently grows as Azure adds operations to the provider, which is a grant that widens over time without a code change.


Built-in roles — the ones that carry most of the weight

There are hundreds ⚠️ verify the current count against current Azure docs, and they fall into two families that behave very differently.

The three broad management roles

Role Grants Notably does not grant
Owner Every control-plane action, including writing role assignments Data access. No dataActions at all
Contributor Every control-plane action except writing role assignments and deny assignments Role assignment. And data access
Reader Read every control-plane property Any write. Any data

Contributor is the workhorse and the trap. It can create, modify, and delete every resource in scope — including deleting the production database — while being unable to grant anyone else access. Teams reach for it reasoning "they're not Owner, so it's contained", which misreads what containment means here. The narrower path is a service-specific Contributor (Storage Account Contributor, Virtual Machine Contributor, Website Contributor) plus the specific data role, and it's usually about ten minutes more work.

Two roles worth knowing by name because they're the ones that let someone escalate:

  • User Access Administrator — may write role assignments but nothing else. This is Owner's dangerous half, isolated. Anyone holding it can grant themselves anything.
  • Role Based Access Control Administrator — a narrower version, constrained so it can only assign a limited set of roles ⚠️ verify current behaviour and constraints against current Azure docs. Prefer it over User Access Administrator where it fits.

Service-specific and data-plane roles

The useful ones. Note the naming convention — the word Data in a role name is the tell that it carries dataActions:

Role Plane What it actually permits
Storage Account Contributor Control Manage the account: keys, networking, redundancy. Not blob contents (though it can read the keys, which is a back door)
Storage Blob Data Reader Data Read blobs and list containers
Storage Blob Data Contributor Data Read, write, delete blobs
Key Vault Contributor Control Manage the vault resource. Not read a secret
Key Vault Secrets User Data Read secret values
Azure Service Bus Data Sender Data Send messages to a queue or topic
AcrPull Data Pull images from a container registry
Virtual Machine Contributor Control Manage VMs, but not the VNet or the subscription
Monitoring Reader Control Read metrics, logs, and alerts across the scope

The pattern is consistent enough to be a rule: if you need to touch contents, look for a role with "Data" in the name. If one doesn't exist for the service, the service probably doesn't have RBAC on its data plane yet and you're stuck with its native mechanism.


Scope and inheritance

Analogy: grants flow downhill like water. Never uphill, and never around.

Technically: scope is a resource ID. Four levels are always available, plus service-specific sub-resource scopes:

/                                                                    ← root scope; requires elevate access
└── /providers/Microsoft.Management/managementGroups/mg-corp
    └── /subscriptions/{sub}
        └── /subscriptions/{sub}/resourceGroups/rg-app
            └── .../providers/Microsoft.Storage/storageAccounts/stapp
                └── .../blobServices/default/containers/uploads      ← sub-resource scope

A Reader assignment at the subscription makes you a Reader on every resource group and resource inside it, forever, including resources created tomorrow. This is what makes RBAC pleasant to operate and dangerous to over-grant.

You cannot subtract at a lower scope. There is no deny, so an over-broad assignment high in the tree cannot be walked back locally. It must be removed where it was made. That asymmetry is the whole argument for the practical rule:

Assign at the narrowest scope that works. Assign high only for genuinely platform-wide roles.

Resource group is the right default for a workload team. Subscription is right for a platform team's Reader and for policy-adjacent roles. Management group is right for organisation-wide security readers and almost nothing else. Root scope (/) is right for nothing you do deliberately.

Sub-resource scopes are the underused tool. A role assignment on a single blob container or a single queue is supported for several services and is dramatically narrower than the account:

az role assignment create \
  --assignee-object-id $OID --assignee-principal-type ServicePrincipal \
  --role "Storage Blob Data Reader" \
  --scope "$(az storage account show -g rg-app -n stapp --query id -o tsv)/blobServices/default/containers/uploads"

Answering "why does this person have access?"

The flag that matters is --include-inherited, because the assignment causing the access is often not visible on the resource itself:

az role assignment list -g rg-app --include-inherited -o table
az role assignment list --assignee <object-id> --all -o table

Custom roles

Analogy: writing your own job description because none of the standard ones fit.

Technically: the same four arrays plus assignableScopes — the list of management groups or subscriptions in which the role may be used. Custom roles are tenant-level objects with a cap per tenant ⚠️ verify the current limit against current Azure docs.

{
  "Name": "VM Restart Operator",
  "Description": "Restart and read VMs. No resize, no delete, no disk changes.",
  "Actions": [
    "Microsoft.Compute/virtualMachines/read",
    "Microsoft.Compute/virtualMachines/restart/action",
    "Microsoft.Compute/virtualMachines/instanceView/read",
    "Microsoft.Resources/subscriptions/resourceGroups/read"
  ],
  "NotActions": [],
  "DataActions": [],
  "NotDataActions": [],
  "AssignableScopes": ["/subscriptions/00000000-0000-0000-0000-000000000000"]
}

Write one when a built-in role is genuinely too broad for a standing grant. The support team that must restart VMs but not resize or delete them is the canonical case, and there's no built-in role that does it.

Don't write one to shave a single permission off Contributor. You now own a role definition that must be maintained as Azure adds operations to the providers it covers, and the failure mode is a capability quietly missing months later, diagnosed as a bug. Custom roles are a maintenance commitment, not a config change.

Two constraints worth knowing before you start: assignableScopes cannot include the root scope / for custom roles, and wildcards in actions are supported but a bad idea for exactly the reason above.


ABAC conditions — narrowing an assignment at request time

Analogy: the card reader also checks the label on the box you're carrying.

Technically: a condition is an expression attached to a role assignment, evaluated per request against attributes of the principal, the resource, the request, or the environment. The most useful attributes today are blob index tags and path prefixes, so one Storage Blob Data Reader assignment can be limited to blobs under a prefix rather than the whole container:

(
 (!(ActionMatches{'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read'}))
 OR
 (@Resource[Microsoft.Storage/storageAccounts/blobServices/containers/blobs:path] StringStartsWith 'reports/2026/')
)

The shape is always the same and worth reading once: either this isn't the action I'm constraining, or the constraint holds. That double-negative form is how a condition avoids accidentally granting anything.

Support is per-service and still expanding ⚠️ verify current coverage against current Azure docs. Where it works it's a much better answer than proliferating containers to fit the permission model. Where it doesn't, reshaping the resources so the scope boundary matches the permission boundary is the honest fallback.


Deny assignments — real, and not yours

A deny assignment blocks specific actions for specific principals at a scope, and it outranks every role assignment. The catch: you cannot author one. They are created by the platform — Azure managed applications create them so the publisher's resources can't be tampered with, and Blueprints did before it was deprecated.

You will see them in az role assignment list output and in the portal's Deny assignments tab. The practical implication is diagnostic only: if an Owner is being refused an action and there's no lock and no policy, check for a deny assignment before assuming a bug.


Directory roles vs. Azure roles — the table to memorise

Entra directory roles Azure RBAC roles
Govern The tenant: users, groups, app registrations, licences, domains Azure resources: VMs, storage, vaults, subscriptions
Scoped to The tenant, or an administrative unit Management group / subscription / resource group / resource
Examples Global Administrator, User Administrator, Application Administrator Owner, Contributor, Reader, Storage Blob Data Reader
Stored in The directory ARM
Managed via Microsoft Graph, the Entra ID blade, az ad, azuread provider ARM, the Access control (IAM) blade, az role, azurerm provider
Inherits No hierarchy to inherit through Downward through the resource hierarchy

They are disjoint but for one deliberate bridge. A Global Administrator can "elevate access", which grants them User Access Administrator at the root scope / — the ability to assign Azure roles anywhere in the tenant. It is a break-glass path, it is recorded in the activity log, and it should be alarmed on. If someone asks why their Global Admin can't see the subscriptions, this is the answer, and "just elevate" is usually the wrong one.

# Break-glass, and it should page someone
az rest --method post --url "https://management.azure.com/providers/Microsoft.Authorization/elevateAccess?api-version=2016-07-01"

Privileged Identity Management — the time axis

Everything above narrows what and where. PIM narrows when.

An eligible assignment is one the principal can activate rather than one they hold. Activation requires justification, optionally an approver, optionally MFA, and expires after a configured window. PIM covers both directory roles and Azure roles, and the Azure-role half is exposed through ARM as roleEligibilityScheduleRequests and roleAssignmentScheduleRequests — which means Terraform can manage it.

Requires Microsoft Entra ID P2 or Entra ID Governance ⚠️ verify the current licensing requirement against current Azure docs. The licence is per privileged user, which usually makes it far cheaper than people assume, and it is the correct answer to "who should be a standing Owner" — nobody, they should activate for four hours when they need it.

Access reviews are the same licence family and the other half of the story: a recurring attestation that the people holding a grant still need it. Without one, an access model decays by accretion.


The vocabulary traps, collected

These sound alike And are not
Directory role / Azure role Different systems, different stores, disjoint effects
App registration / Enterprise application The global app definition vs. its service principal in this tenant. Permissions granted on the wrong one is the classic "I granted it but it doesn't work"
notActions / a deny Subtraction within one role vs. an override across all of them. Only the latter would be a boundary, and you can't write it
Owner / Global Administrator Resources vs. the directory. Neither implies the other
Role definition / role assignment The job description vs. the act of giving someone the job
Key Vault Contributor / Key Vault Secrets User Manage the vault vs. read a secret. The first cannot do the second
Azure RBAC / Azure Policy Who may act vs. what may exist
Deny assignment / resource lock Platform-created per-principal block vs. a scope-wide block on everyone including Owner

Next: Architecture →

← Back to the Azure RBAC overview · ← Previous: What & Why