Azure Identity & Access

Azure RBAC Fundamentals: Roles, Scopes and Assignments Without the Confusion

Someone on your team can’t restart a VM. Someone else just deleted a resource group they should never have been able to touch. A third person has Owner on the whole subscription because two years ago it was “just easier.” Every one of these is the same problem wearing a different mask: Azure role-based access control (RBAC) — the system that decides who can do what, to which resources — is mis-modelled. Not broken, not buggy. Mis-modelled, because the person who set it up reasoned about “permissions” as a flat list of yes/no toggles instead of the three-part machine RBAC actually is.

That machine has exactly three moving parts, and once you see them you stop guessing. A role definition is a named bundle of allowed operations (Actions, DataActions, and their NotActions/NotDataActions subtractions). A scope is where that bundle applies — a management group, subscription, resource group, or single resource — and it inherits downward. A role assignment binds them: this security principal gets this role definition at this scope. Access = the union of every assignment that touches the resource you’re reaching. That’s the whole model; everything else — Owner vs Contributor, custom roles, deny assignments, “why isn’t my assignment working” — is a consequence of those three nouns and one verb (union).

This guide builds that model and makes it real. You will create a custom role, assign built-in and custom roles at every scope level, and prove the inheritance and union rules by watching access change — in the portal, the az CLI, and as Bicep, end to end, with expected output at each step and a clean teardown. By the end, the three masks above resolve to the same root cause, and you can fix each in under a minute. If you have ever stared at the Access control (IAM) blade wondering why someone who “has the role” still gets a 403, this is the article that ends that.

What problem this solves

In a real Azure tenant, access is the highest-stakes configuration you own. Too tight and engineers file tickets to restart their own dev VMs, ship slows, and people hoard broad roles to dodge the friction. Too loose and one compromised credential — or one fat-fingered az group delete — takes out production. The primitives are simple; the combinations hurt: a Contributor at subscription scope silently overrides the careful Reader you set at a resource group, a typo’d Action wildcard grants far more than intended, and a Blob Data Reader assignment doesn’t help because the caller needed the control-plane role, not the data-plane one.

What breaks without a clear model: people conflate management-plane permissions (manage the resource — create, delete, configure, via Azure Resource Manager) with data-plane permissions (read/write the data inside the resource — blobs, secrets, queue messages). Owner on a storage account lets you delete it and rotate its keys, but historically did not let you read a blob without a data role — a distinction that has produced more confused 403s than any other single thing in Azure. People also forget that RBAC is additive and union-based with no explicit deny in a normal assignment — you cannot “subtract” access with a more-restrictive role — and that scope inherits: an Owner at the management group is an Owner of every subscription, resource group and resource underneath, until someone removes it.

Who hits this: every team past the first month on Azure, hardest at the boundary between “we’re five people and everyone’s an Owner” and “we’re an org with auditors, separation of duties, and a least-privilege mandate.” The fix is never “give them Owner” and never “file a ticket every time” — it is “assign the narrowest role that does the job, at the narrowest scope that covers the need.”

Before the deep dive, here is the field in one table — the three nouns, what each one is, and the single most common mistake people make with it:

RBAC primitive What it is Lives at Most common mistake
Role definition A named set of allowed operations (Actions/DataActions minus NotActions) Tenant (built-in) or a subscription/MG (custom) Using a wildcard Action that grants more than intended
Scope Where a role applies; inherits downward MG → Subscription → Resource group → Resource Assigning at subscription when a resource group would do
Role assignment Principal + role + scope, bound together Any scope Forgetting it’s additive — a broad one overrides a narrow one
Security principal The who: user, group, service principal, or managed identity Entra ID Assigning to a user instead of a group (unmanageable at scale)
Deny assignment An explicit “deny”, created by Azure (e.g. Blueprints/managed apps) — you don’t author these directly Any scope Assuming you can hand-author one (you cannot, in normal RBAC)

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You should already know what an Azure subscription, resource group and resource are, and that they nest — if not, read Azure Resource Hierarchy Explained: Subscriptions, Resource Groups and Resources first, because RBAC scope is that hierarchy. You should be able to run az in Cloud Shell, read JSON output, and have at least User Access Administrator or Owner on a subscription or resource group you can experiment in (you cannot grant access you don’t yourself have the right to grant). Basic familiarity with Microsoft Entra ID (formerly Azure AD) — users, groups, service principals — helps, because RBAC grants are to Entra principals.

This sits at the centre of the Identity & Governance track. RBAC is the access layer that everything else assumes: Management Groups 101: Designing a Hierarchy That Scopes Policy and RBAC is the scope you assign above subscriptions; Azure Policy Effects Decoded: Deny vs Audit vs Modify vs DeployIfNotExists governs what configuration is allowed (a different axis from who can act); and Managed Identities Demystified: System vs User-Assigned and When to Use Each gives non-human workloads the very principals you assign RBAC roles to. Two cousins worth flagging up front, because conflating them with RBAC is itself a common mistake:

Sounds like RBAC, but isn’t What it actually does The axis it controls Where it overlaps RBAC
Azure Policy Allows/denies/audits resource configuration (e.g. “no public IPs”) What may be deployed Both can use deny-style effects, but on different things
Conditional Access Gates sign-in (MFA, device, location) How you authenticate Runs before RBAC; RBAC runs after you’re authenticated
Key Vault access policies Legacy data-plane permissions for one vault Data inside that vault Superseded by RBAC data roles — see the Key Vault article
Management group / RG locks Block delete/modify regardless of role Mutability of resources A CanNotDelete lock stops even an Owner
Entra roles (e.g. Global Admin) Manage the directory (users, groups, apps) The tenant itself Distinct from Azure RBAC; a Global Admin is not an Azure Owner by default

That last row catches senior people: Entra directory roles (Global Administrator) and Azure RBAC roles (Owner) are separate systems — a Global Admin has no access to Azure resources until they elevate access at the tenant root or get an Azure role assignment.

Core concepts

Five mental models make every later step obvious.

A role definition is a permission bundle, not a permission. A role like Contributor is a named JSON document of operation strings. Its fields: Actions (control-plane ops it grants), NotActions (ops subtracted from those — Contributor has Microsoft.Authorization/*/Write here, which is exactly why a Contributor cannot grant access), DataActions (data-plane ops), NotDataActions (subtractions from those), and AssignableScopes (where a custom definition may be assigned). Effective permission = (Actions − NotActions) for control plane, (DataActions − NotDataActions) for data plane. NotActions is a subtraction from the same role, not a deny — another assignment can still grant what one role’s NotActions removed.

Scope is the hierarchy, and it inherits downward. Four levels nest strictly: management group → subscription → resource group → resource. An assignment applies to its level and everything beneath it — Reader at a resource group reads every resource in it; Owner at a management group owns every sub, RG and resource under it. There is no upward inheritance and no per-grant scope exclusion: you cannot say “Owner on the subscription except this one RG” with a single grant — you assign at the narrower scope instead.

A role assignment is a three-tuple, and access is their union. An assignment binds a security principal (who), a role definition (what), and a scope (where). Effective access to any resource is the union of every assignment whose scope covers it. RBAC is additive — there is no author-able deny. A user with Reader at the subscription and Contributor at one RG is a Contributor there and a Reader everywhere else; grants add, never subtract. This single rule explains most “why does this person have more access than I gave them” surprises: someone made a broader assignment at a higher scope.

Control plane and data plane are different doors. Azure Resource Manager governs the management plane — create, delete, configure, and read resource metadata (Actions). The data plane is the data inside the resource — blobs, Key Vault secrets, Service Bus messages (DataActions). A control-plane role like Owner or Contributor does not grant data-plane access for data services: reading a blob needs Storage Blob Data Reader, reading a secret needs Key Vault Secrets User. The separation is deliberate (an admin who manages a storage account shouldn’t automatically read customer data) and is the single most common source of unexpected 403s.

The principal is one of four kinds, and you almost always assign to a group. A principal is a user, a group (assign here for anything beyond a one-off), a service principal (an app/automation identity in Entra), or a managed identity (an Azure-managed SP for a workload). Every assignment targets one by its object ID (a GUID). Assigning to individual users is the road to an unauditable mess; assign to groups and manage membership — that single discipline is what makes RBAC survive an org of 500 engineers.

The vocabulary in one table

Pin down every moving part before the deep sections. The glossary at the end repeats these for lookup; this is the model side by side:

Term One-line definition Where it lives Why it matters
Role definition Named bundle of allowed operations Built-in (tenant) or custom (MG/sub) The what of an assignment
Actions Control-plane ops the role grants Inside the role definition Manage/configure/delete the resource
NotActions Ops subtracted from this role’s Actions Inside the role definition A subtraction, not a deny
DataActions Data-plane ops the role grants Inside the role definition Read/write data inside the resource
AssignableScopes Where a (custom) role may be assigned Inside the role definition Limits a custom role to your sub/MG
Scope Where an assignment applies; inherits down MG / Sub / RG / Resource The where of an assignment
Role assignment Principal + role + scope Any scope The grant itself
Security principal The who being granted Entra ID User / group / SP / managed identity
Built-in role A Microsoft-authored role definition Tenant ~150+ ready-made roles
Custom role A role definition you author A subscription or MG When no built-in fits
Deny assignment Azure-created explicit deny Any scope Overrides allow; you don’t author it
PIM Privileged Identity Management — eligible/just-in-time roles Entra ID P2 Time-bound, approved elevation
ABAC Attribute-based conditions on an assignment On the assignment Narrow a role by tag/attribute

Built-in roles: the ones you actually use

Azure ships 150+ built-in roles, but four general-purpose ones plus two access-management ones cover the overwhelming majority of real assignments. Know these cold; reach for a service-specific role (like Storage Blob Data Reader) when you need data-plane access. Here are the general-purpose control-plane roles and exactly what separates them:

Role Control-plane access Can grant access to others? Data-plane access Use it for
Owner Full — manage everything Yes (Microsoft.Authorization/*) No (still needs data roles) Account/sub owners; keep this list tiny
Contributor Full — manage everything No (NotActions blocks Authorization/*/Write) No Engineers who build/operate but shouldn’t grant access
Reader Read-only on everything No No Auditors, dashboards, on-call read access
User Access Administrator Read everything + manage access Yes (assignments only) No Delegating access management without resource control
Role Based Access Control Administrator Manage access, but cannot grant Owner/UAA beyond constraints Yes (constrained) No Newer, safer alternative to UAA for delegating RBAC

The crucial distinctions: Contributor can do everything except give other people access — its NotActions strip Microsoft.Authorization/*/Write and */Delete, so it builds, breaks and operates resources but cannot escalate to Owner. Owner is Contributor plus access management — why you treat it like root. User Access Administrator is the surgical “manage who has access, but not the resources” role for a security team. Role Based Access Control Administrator is its narrower cousin: it manages assignments but is itself constrained (limitable to specific roles), closing the long-standing gap where a UAA holder could simply assign themselves Owner.

Beyond the big six, the data-plane roles are where the “my assignment isn’t working” confusion concentrates. A representative sample, with the control-plane role people wrongly reach for instead:

Service Data-plane role (what you need) What it grants Wrong choice people make
Storage (blobs) Storage Blob Data Reader / Contributor / Owner Read / read-write / full on blob data Contributor on the account (no blob read)
Key Vault (RBAC mode) Key Vault Secrets User Read secret values Contributor on the vault (no secret read)
Key Vault (RBAC mode) Key Vault Administrator Full data-plane on keys/secrets/certs Owner on the vault (manages, can’t read secrets)
Service Bus Azure Service Bus Data Sender / Receiver Send / receive messages Contributor (manages namespace, can’t send)
Cosmos DB (SQL API) Cosmos DB Built-in Data Contributor Read-write items (data plane) Contributor (manages account, no item access)
Azure SQL (uses its own DB-level auth, not RBAC data roles) Expecting an RBAC role to grant table access

Two reading notes that save hours:

Distinction The trap How to remember it
Control plane vs data plane Owner/Contributor manage the resource but can’t read its data “Manage the box” ≠ “read what’s in the box”
* Actions vs specific Actions A wildcard role grants future operations too Owner’s * means everything, including new services
UAA vs RBAC Administrator UAA can self-escalate to Owner; RBAC Admin can be constrained Prefer RBAC Administrator for delegating grants

Anatomy of a role definition

Every role — built-in or custom — is the same JSON shape. Understanding it is what lets you read a built-in role to see exactly what it grants, and author a custom one safely. Here is the canonical structure, using a trimmed real example:

{
  "Name": "Virtual Machine Operator",
  "Description": "Start, stop, restart and read VMs — but not create, delete or resize them.",
  "Actions": [
    "Microsoft.Compute/virtualMachines/read",
    "Microsoft.Compute/virtualMachines/start/action",
    "Microsoft.Compute/virtualMachines/restart/action",
    "Microsoft.Compute/virtualMachines/powerOff/action",
    "Microsoft.Compute/virtualMachines/instanceView/read",
    "Microsoft.Resources/subscriptions/resourceGroups/read"
  ],
  "NotActions": [],
  "DataActions": [],
  "NotDataActions": [],
  "AssignableScopes": [
    "/subscriptions/00000000-0000-0000-0000-000000000000"
  ]
}

Read it field by field, because each one has a gotcha:

Field What it means Gotcha / rule
Name / roleName Display name Must be unique within the tenant for custom roles
Description Human description Shows in the portal; write it for the next engineer
Actions Control-plane ops granted Strings are Provider/resourceType/operation; * is a wildcard
NotActions Ops subtracted from Actions A subtraction from this role, not a global deny
DataActions Data-plane ops granted Only meaningful for data services; absent = no data access
NotDataActions Ops subtracted from DataActions Same subtraction semantics, data plane
AssignableScopes Scopes where this role can be assigned Custom roles only; built-ins are implicitly all scopes

The operation string grammar is how you reason about least privilege. A string is {Provider}/{resourceType}/{operation} — e.g. Microsoft.Storage/storageAccounts/listkeys/action (control-plane) versus Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read (a DataAction). Suffixes carry meaning:

Operation suffix Means Example
/read Read/list this resource type Microsoft.Compute/virtualMachines/read
/write Create or update Microsoft.Compute/virtualMachines/write
/delete Delete Microsoft.Storage/storageAccounts/delete
/action A specific verb/operation .../virtualMachines/restart/action
* (wildcard) All operations at that level Microsoft.Compute/* = everything in Compute

The wildcard is the foot-gun: Microsoft.Storage/* in Actions grants every current and future Storage control-plane operation — so an operation Azure adds next year is auto-granted. For least privilege, enumerate specific operations; for a deliberately broad management role, the wildcard is fine. Discover the exact strings a provider exposes with:

# List every operation a resource provider exposes (the building blocks of a role)
az provider operation show --namespace Microsoft.Compute \
  --query "resourceTypes[?name=='virtualMachines'].operations[].{op:name, display:displayName}" -o table

Scope and inheritance: where access actually lands

Scope is the second noun, and it is where most over-grants come from. The four levels, top to bottom, with the format of the scope string you’ll feed to az and Bicep:

Scope level Scope string format What an assignment here covers Typical use
Management group /providers/Microsoft.Management/managementGroups/<mgId> Every subscription/RG/resource under the MG Org-wide platform/security roles
Subscription /subscriptions/<subId> Every RG and resource in the subscription Sub owners, broad operator roles
Resource group /subscriptions/<subId>/resourceGroups/<rg> Every resource in that RG The default, right-sized scope for a team/app
Resource /subscriptions/<subId>/resourceGroups/<rg>/providers/<rp>/<type>/<name> Just that one resource Surgical, single-resource grants

The inheritance rule is absolute and one-directional: an assignment flows down, never up. Concretely:

You assign… At this scope The principal can act on… But NOT on…
Reader Subscription Every RG and resource in the sub Other subscriptions, the MG
Contributor Resource group rg-app Every resource in rg-app Resources in other RGs
Owner Management group mg-prod Every sub/RG/resource under mg-prod Subs outside mg-prod
Storage Blob Data Reader One storage account Blob data in that account Other storage accounts

Two consequences trip people up. First, there is no “deny here” within a grant — assign Owner at the subscription and you cannot “remove” one RG by adding a Reader there (the Owner still applies); to genuinely restrict, assign at the narrower scope to begin with. Second, higher-scope assignments are easy to forget: when someone “has more access than expected,” walk up the tree — az role assignment list --scope <resource> --include-inherited shows every inherited parent-scope grant, which is where the surprise almost always lives.

# Show ALL assignments affecting a resource group, including inherited from sub/MG
az role assignment list \
  --resource-group rg-app-prod \
  --include-inherited \
  --query "[].{principal:principalName, role:roleDefinitionName, scope:scope}" -o table

For the deeper hierarchy mechanics — how management groups nest and why you scope org-wide roles there — see Management Groups 101: Designing a Hierarchy That Scopes Policy and RBAC.

Custom roles: when no built-in fits

Reach for a custom role only when no built-in (or combination) does the job — each one is more to maintain and audit. The legitimate cases: you need fewer permissions than the smallest built-in (e.g. “restart VMs only”), a precise cross-service bundle, or to enforce assignability within specific scopes via AssignableScopes. The decision in one table:

Situation Use a built-in role? Use a custom role? Why
“Read everything” Reader No Built-in exists and is perfect
“Manage VMs fully” Virtual Machine Contributor No Built-in service role exists
“Restart VMs, nothing else” No built-in is this narrow Yes Least privilege below any built-in
“Read blobs in one account” Storage Blob Data Reader No Data role exists
“Manage tags but not resources” Tag Contributor No Built-in exists
“Bespoke cross-service operator” Usually no single fit Yes Combine specific Actions you enumerate

Custom-role limits and rules you must respect:

Rule / limit Value Consequence if ignored
Custom roles per tenant 5,000 (Azure cloud) Hard cap; design for reuse, not per-team copies
Where it’s stored A subscription or management group Defined once, assignable across listed scopes
AssignableScopes required At least one scope The role can only be assigned within these
Wildcard in DataActions Restricted * in DataActions is intentionally limited for safety
Editing a built-in role Not allowed Clone it to a custom role and edit the copy
Who can create one Owner or User Access Administrator / RBAC Admin at the scope Insufficient rights → AuthorizationFailed

A practical authoring tip: don’t write a custom role from a blank page — clone the nearest built-in and trim it. Export a built-in role’s JSON, save it as your starting point, strip the Actions you don’t want, set AssignableScopes, and create:

# Export the built-in "Virtual Machine Contributor" as a JSON starting point
az role definition list --name "Virtual Machine Contributor" \
  --query "[0].{Actions:permissions[0].actions, NotActions:permissions[0].notActions}" -o json

Architecture at a glance

Walk the access decision left to right and the whole model clicks. A principal — a user (often via group membership), a service principal, or a workload’s managed identity — issues a request to Azure Resource Manager or a data endpoint. Microsoft Entra ID authenticates it, optionally gated by Conditional Access (MFA, device, location) — before RBAC and on a separate axis. The caller then reaches the RBAC evaluation engine: it gathers every role assignment whose scope covers the target — walking the tree from management group down through subscription and resource group to the resource — and takes their union. If any deny assignment matches, deny wins regardless of allows. Otherwise, if the union includes the requested operation, ARM permits it and the action hits the target resource (control plane) or its data endpoint (data plane, governed by DataActions).

The diagram below shows that path end to end, and its five numbered badges mark the places an access request most commonly goes wrong — wrong scope, control-vs-data-plane mismatch, propagation/token lag, a deny assignment, and an additive over-grant from a parent scope — with the legend mapping each to its symptom, confirm step, and fix.

Azure RBAC access-evaluation architecture: a user, service principal and managed identity on the left authenticate through Microsoft Entra ID and Conditional Access, then reach the RBAC evaluation engine which unions all role assignments across the management group, subscription, resource group and resource scope tree (inheritance flowing downward), checks deny assignments, and resolves to either management-plane access on the target resource or data-plane access on its data endpoint; five numbered failure-point badges mark wrong scope, control-vs-data-plane mismatch, propagation delay, deny-assignment override and additive over-grant.

Real-world scenario

Northwind Logistics (a fictional but representative mid-size shipper) moved to Azure with the classic anti-pattern: one production subscription and eleven people with Owner, because the founders each added engineers as Owner “to unblock them.” Pursuing SOC 2, the auditor’s first finding was blunt: no least privilege, no separation of duties, Owner sprawl with no justification. Three weeks to remediate without breaking daily work.

The platform lead, Asha, started by seeing the truth. az role assignment list --all --include-inherited --query "[?roleDefinitionName=='Owner']" returned the eleven Owners plus — the real shock — an Owner at the management group nobody remembered, inherited down to three subscriptions, silently granting two contractors Owner on production. Lesson one: always walk up the scope tree; the worst over-grants hide at parent scopes.

She then modelled what people actually did. The four backend engineers built and operated app resources — Contributor at the two app resource groups, not Owner on the subscription. The two SREs needed read-everywhere plus restart rights — Reader at the subscription plus a small custom “VM Operator” role (start/stop/restart only) at the compute RG. The data team needed blobs in the analytics account — Storage Blob Data Reader on that one account, which (lesson two) none had, because they’d relied on Owner to read data, not realising Owner doesn’t grant blob reads. Access management went to a two-person security group with User Access Administrator, later swapped for Role Based Access Control Administrator so they couldn’t self-escalate to Owner.

Everything was assigned to Entra groups, never individuals (grp-backend-eng, grp-sre, grp-data, grp-access-admins); membership became the only daily-managed thing, and the assignments went into Bicep, reviewed in PRs. The two genuine subscription Owners were moved to PIM-eligible — activating Owner for eight hours with justification, Reader the rest of the time.

The numbers: Owners dropped from eleven (plus two hidden) to two eligible, in nine working days, because the model was simple once the inherited grant was found. The finding closed. Six months later a contractor’s laptop was phished — and because that contractor was a Contributor on one resource group, not an Owner on the subscription, the blast radius was one app’s resources, not the whole production estate. The least-privilege model paid for itself in a single incident.

Advantages and disadvantages

RBAC done well is one of the highest-leverage controls in Azure, but it has real friction. The honest trade-off:

Advantages Disadvantages
Fine-grained, least-privilege access by operation Granularity adds modelling effort up front
Scope inheritance scales one grant across many resources Inheritance also makes over-grants easy to forget
Built-in roles cover most needs with zero authoring Built-ins can be coarser than you want (forces custom roles)
Group-based assignments scale to thousands of people Easy to mis-target individuals and create audit sprawl
Native to ARM — works across portal, CLI, IaC Control-vs-data-plane split confuses newcomers (surprise 403s)
Additive/union model is simple to reason about No author-able explicit deny for “everything except X”
Integrates with PIM for just-in-time elevation PIM/ABAC require higher Entra licences (P2)
Auditable via assignment lists and activity logs Propagation/membership lag makes “is it applied yet?” fuzzy

When each side matters: granularity and inheritance dominate at scale — a landing zone with org-wide platform roles at the management group and right-sized team roles at resource groups depends on them. The disadvantages bite hardest early, when a small team feels the modelling overhead and reaches for Owner to skip it — the exact decision that becomes the audit finding later. The control-vs-data-plane split is a one-time learning cost. The lack of an author-able deny is rarely a real limitation — it pushes you toward the correct pattern (assign at the narrower scope) rather than the fragile one (broad grant minus exceptions).

Hands-on lab

This is the centrepiece. You will build the entire RBAC model end to end — create a group, assign a built-in role at resource-group scope, author and assign a custom role, prove inheritance and the additive union rule by observing access, and tear it all down. Everything is free-tier-friendly: RBAC assignments themselves cost nothing, and the only resource we create is a single empty storage account (delete it at the end; a few minutes of an empty account is well under ₹1). Do the lab in both the portal and az CLI, then review the Bicep equivalent. Run the CLI parts in Cloud Shell (Bash).

Prerequisites: you need Owner or User Access Administrator (or Role Based Access Administrator) on a subscription or resource group you can experiment in — you cannot create assignments you lack the right to create. You also need permission to create an Entra group (or an existing test group’s object ID). If you can only operate within one resource group, scope everything to it.

Part A — Portal: assign a built-in role at resource-group scope

Step 1 — Create a resource group to scope into. Portal → Resource groupsCreate → name rg-rbac-lab, region Central India (or your nearest), Review + createCreate.

Expected: the resource group appears in the list within a few seconds.

Step 2 — Create a test Entra group. Portal → Microsoft Entra IDGroupsNew group → Group type Security, Group name grp-rbac-lab-readers, Create. (If you lack group-create rights, ask an admin or reuse an existing test group and note its name.)

Expected: the group is listed under Groups. This is the principal you’ll grant a role to — a group, not a person, by design.

Step 3 — Assign Reader at the resource-group scope. Open rg-rbac-labAccess control (IAM)+ AddAdd role assignment. On the Role tab choose Reader. On the Members tab: Assign access to = User, group, or service principal, + Select members, pick grp-rbac-lab-readers. Review + assign.

Expected: a green “Added role assignment” notification. On the IAM blade’s Role assignments tab, grp-rbac-lab-readers now shows as Reader with Scope = This resource.

Step 4 — Read the assignment back and confirm inheritance display. Still on Access control (IAM)Role assignments, note the Scope column. Now switch the filter Scope = All scopes (or use the Check access tab): assignments inherited from the subscription or management group appear with a scope of (Inherited). This is the single most useful habit in RBAC — seeing what’s inherited versus assigned here.

Expected: your new Reader shows scope rg-rbac-lab; any subscription-level Owner/Contributor you have shows as inherited. You’ve just visually confirmed the inheritance rule.

Part B — az CLI: variables, IDs, and a built-in assignment

Step 5 — Set variables and capture IDs. In Cloud Shell:

RG=rg-rbac-lab
LOC=centralindia
SUB_ID=$(az account show --query id -o tsv)
GROUP_ID=$(az ad group show --group "grp-rbac-lab-readers" --query id -o tsv)
echo "Subscription: $SUB_ID"
echo "Group object ID: $GROUP_ID"

Expected: a subscription GUID and the group’s object ID print. The object ID (not the display name) is what assignments target under the hood.

Step 6 — Create a storage account to use as a fine-grained scope and a data-plane target.

SA=strbaclab$RANDOM           # must be globally unique, lowercase, 3-24 chars
az storage account create -n $SA -g $RG -l $LOC --sku Standard_LRS -o table
SA_ID=$(az storage account show -n $SA -g $RG --query id -o tsv)
echo "Storage account scope: $SA_ID"

Expected: a storage account row with sku.name = Standard_LRS. SA_ID is now a resource-level scope string.

Step 7 — Assign a built-in role at resource-group scope via CLI (mirrors Part A).

az role assignment create \
  --assignee-object-id $GROUP_ID \
  --assignee-principal-type Group \
  --role "Reader" \
  --scope "/subscriptions/$SUB_ID/resourceGroups/$RG" -o table

Expected: a JSON/table row with roleDefinitionName = Reader, principalId = <GROUP_ID>, scope = .../resourceGroups/rg-rbac-lab. (Using --assignee-object-id + --assignee-principal-type avoids a Graph lookup and the occasional PrincipalNotFound race for freshly created principals.)

Step 8 — List assignments to prove the grant landed and inspect inheritance.

# Assignments AT this resource group plus everything inherited from sub/MG
az role assignment list \
  --scope "/subscriptions/$SUB_ID/resourceGroups/$RG" \
  --include-inherited \
  --query "[].{principal:principalName, role:roleDefinitionName, scope:scope}" -o table

Expected: your Reader for the group at the RG scope, plus any inherited Owner/Contributor from the subscription shown with the subscription scope. This is the CLI version of Step 4 — and the command you’ll run in every real “why does this person have access” investigation.

Part C — Author and assign a custom role

Step 9 — Write the custom role definition JSON. This role grants only the ability to start, stop, restart and read VMs — narrower than any built-in. Create the file in Cloud Shell:

cat > vm-operator-role.json <<EOF
{
  "Name": "VM Operator (Lab)",
  "Description": "Start, stop, restart and read virtual machines only. No create/delete/resize.",
  "Actions": [
    "Microsoft.Compute/virtualMachines/read",
    "Microsoft.Compute/virtualMachines/start/action",
    "Microsoft.Compute/virtualMachines/restart/action",
    "Microsoft.Compute/virtualMachines/powerOff/action",
    "Microsoft.Compute/virtualMachines/instanceView/read",
    "Microsoft.Resources/subscriptions/resourceGroups/read"
  ],
  "NotActions": [],
  "DataActions": [],
  "NotDataActions": [],
  "AssignableScopes": [
    "/subscriptions/$SUB_ID"
  ]
}
EOF

Note: AssignableScopes is set to your subscription, so this custom role can only ever be assigned within it — a guardrail built into the role itself.

Step 10 — Create the custom role.

az role definition create --role-definition vm-operator-role.json -o table

Expected: a row showing roleName = VM Operator (Lab) and a generated name GUID (the role definition ID). Custom-role creation can take a few seconds to a minute to propagate before it’s assignable.

Step 11 — Verify the custom role exists and read its permissions back.

az role definition list --custom-role-only true \
  --query "[?roleName=='VM Operator (Lab)'].{name:roleName, actions:permissions[0].actions, scopes:assignableScopes}" -o json

Expected: your six Actions and the subscription in assignableScopes. You’ve just proven the role definition is exactly what you wrote — no more, no less.

Step 12 — Assign the custom role at resource-group scope.

az role assignment create \
  --assignee-object-id $GROUP_ID \
  --assignee-principal-type Group \
  --role "VM Operator (Lab)" \
  --scope "/subscriptions/$SUB_ID/resourceGroups/$RG" -o table

Expected: a row with roleDefinitionName = VM Operator (Lab). The group is now Reader + VM Operator in rg-rbac-lab — your live demonstration of the additive union: effective access is the combination of both roles, not one overriding the other.

Part D — Prove control-plane vs data-plane (the 403 lesson)

Step 13 — Confirm the group has NO blob data access despite resource-group access. The group is Reader on the RG and VM Operator — neither is a data role. List the account’s assignments and note the absence of any data role:

# Reader grants metadata read on the storage account (control plane) — NOT blob data
az role assignment list --scope "$SA_ID" --include-inherited \
  --query "[].{principal:principalName, role:roleDefinitionName}" -o table

Expected: the inherited Reader appears, but Reader’s Actions do not include .../blobServices/containers/blobs/read (a DataAction), so a member calling the blob endpoint gets 403 AuthorizationPermissionMismatch — the most common real-world RBAC surprise, reproduced.

Step 14 — Grant the missing data-plane role at the storage-account scope.

az role assignment create \
  --assignee-object-id $GROUP_ID \
  --assignee-principal-type Group \
  --role "Storage Blob Data Reader" \
  --scope "$SA_ID" -o table

Expected: a row with roleDefinitionName = Storage Blob Data Reader, scoped to the storage account only. Now members can read blob data in this one account — control plane and data plane granted by two different roles, exactly as designed. (Data-plane assignments can take a few minutes to propagate to the data endpoint.)

Step 15 — Validate the full picture.

# Every assignment for the group, across all scopes, in one view
az role assignment list --all --assignee $GROUP_ID \
  --query "[].{role:roleDefinitionName, scope:scope}" -o table

Expected: three rows — Reader (RG), VM Operator (Lab) (RG), Storage Blob Data Reader (storage account). You can read the entire access model of the group from this one output: what it can do (read RG, operate VMs, read blobs in one account) and where.

Part E — The Bicep equivalent

For production you wouldn’t click or run ad-hoc CLI — you’d commit the assignments and the custom role as Bicep, reviewed in a PR. Role assignments are the Microsoft.Authorization/roleAssignments resource; the name must be a deterministic GUID (use guid() so re-deploys are idempotent), and roleDefinitionId is the full resource ID of the role.

// rbac.bicep — assign Reader to a group at the resource group's scope
@description('Object ID of the Entra group to grant access to')
param principalId string

// Built-in role definition IDs are well-known GUIDs (Reader shown here)
var readerRoleId = 'acdd72a7-3385-48ef-bd42-f606fba81ae7'

resource readerAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  // Deterministic name → idempotent re-deploys (no duplicate-assignment errors)
  name: guid(resourceGroup().id, principalId, readerRoleId)
  properties: {
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', readerRoleId)
    principalId: principalId
    principalType: 'Group'   // set this so deploys don't fail on principal-propagation races
  }
}

A custom role definition in Bicep (deployed at subscription scope, targetScope = 'subscription'):

targetScope = 'subscription'

resource vmOperator 'Microsoft.Authorization/roleDefinitions@2022-04-01' = {
  name: guid('vm-operator-lab', subscription().id)  // deterministic role ID
  properties: {
    roleName: 'VM Operator (Lab)'
    description: 'Start, stop, restart and read VMs only.'
    type: 'CustomRole'
    permissions: [
      {
        actions: [
          'Microsoft.Compute/virtualMachines/read'
          'Microsoft.Compute/virtualMachines/start/action'
          'Microsoft.Compute/virtualMachines/restart/action'
          'Microsoft.Compute/virtualMachines/powerOff/action'
          'Microsoft.Compute/virtualMachines/instanceView/read'
        ]
        notActions: []
        dataActions: []
        notDataActions: []
      }
    ]
    assignableScopes: [
      subscription().id
    ]
  }
}

Deploy and validate:

# What-if first (preflight: see exactly what will change before it changes)
az deployment group what-if -g $RG -f rbac.bicep --parameters principalId=$GROUP_ID
# Then deploy
az deployment group create -g $RG -f rbac.bicep --parameters principalId=$GROUP_ID -o table

Expected: what-if shows the role assignment being created (or no change if it already exists — proof of idempotency from the deterministic guid() name). For deeper Bicep patterns see Deploy Your First Bicep File From Scratch and the preflight workflow in Bicep what-if: Preflight Validation as a CI Gate.

Validation checklist and teardown

You assigned a built-in role at RG scope (portal + CLI), authored and assigned a custom role, reproduced the control-vs-data-plane 403 and fixed it with the right data role, and saw the additive union of three roles on one group — then expressed it as idempotent Bicep. What each part proved:

Part What you did What it proves
A Reader to a group at RG scope (portal) The three-tuple: principal + role + scope
A/B View inherited assignments Inheritance is real and visible — always check it
C Custom “VM Operator” role + assign Least privilege below any built-in; AssignableScopes guardrail
C Reader + VM Operator on one group Access is the additive union, not override
D Reader present, blob read still 403 Control plane ≠ data plane (the classic surprise)
D Storage Blob Data Reader on the account Data access needs a data role at the data scope
E Bicep with guid() names + what-if Assignments as idempotent, reviewable code

Teardown (assignments and custom roles cost nothing, but clean up the storage account and the artifacts):

# Remove the assignments (optional — deleting the RG removes RG-scoped ones automatically)
az role assignment delete --assignee $GROUP_ID --role "Storage Blob Data Reader" --scope "$SA_ID"
az role assignment delete --assignee $GROUP_ID --role "VM Operator (Lab)" --scope "/subscriptions/$SUB_ID/resourceGroups/$RG"
az role assignment delete --assignee $GROUP_ID --role "Reader" --scope "/subscriptions/$SUB_ID/resourceGroups/$RG"

# Delete the custom role definition (must remove all its assignments first)
az role definition delete --name "VM Operator (Lab)"

# Delete the resource group (removes the storage account and any RG-scoped assignments)
az group delete -n $RG --yes --no-wait

# Optionally delete the test group in the portal: Entra ID → Groups → grp-rbac-lab-readers → Delete

Cost note: the only billable artifact was an empty Standard_LRS storage account — effectively free for the minutes it existed. RBAC role definitions and assignments carry no charge at all.

Common mistakes & troubleshooting

The differentiator. Every entry is a real failure mode: symptom → root cause → how to confirm (exact command/portal path) → fix. Scan the table at 02:14, read the detail when you need it.

# Symptom Root cause Confirm (exact cmd / portal path) Fix
1 403 reading blobs/secrets though user “has Contributor” Control-plane role, no data-plane role az role assignment list --scope <resource> --include-inherited shows no Data role Assign Storage Blob Data Reader / Key Vault Secrets User at the resource
2 User has far more access than you granted A broader assignment at a parent scope (sub/MG) az role assignment list --scope <resource> --include-inherited Remove/narrow the parent-scope grant; assign at the right scope
3 New assignment “isn’t working” yet Propagation delay (seconds–minutes; data plane longer) Re-check after a few minutes; portal IAM shows it present Wait; for data plane allow several minutes
4 User in the right group still denied Group-membership / token lag — old token lacks the group az ad group member check; have them sign out/in Re-issue token (sign out/in); confirm membership
5 Owner blocked from deleting a resource A resource lock (CanNotDelete/ReadOnly), not RBAC az lock list --resource-group <rg> -o table Remove the lock (if appropriate) before the operation
6 Assignment blocked despite Owner A deny assignment (Blueprints/managed app) overrides allow Portal IAM → Deny assignments tab Address the source (managed app/blueprint); deny wins
7 AuthorizationFailed creating an assignment You lack Microsoft.Authorization/roleAssignments/write You’re Contributor (NotActions blocks it), not Owner/UAA Get Owner/User Access Administrator/RBAC Admin at the scope
8 Custom role won’t assign at a scope Scope not in the role’s AssignableScopes az role definition list --name "<role>" --query "[0].assignableScopes" Add the scope to AssignableScopes and update the role
9 PrincipalNotFound on assignment create Principal just created; Graph hasn’t propagated Retry; pass --assignee-object-id + --assignee-principal-type Use object ID + principal type to skip the lookup race
10 Bicep deploy: RoleAssignmentExists / conflict Non-deterministic assignment name re-deployed Assignment name isn’t a stable guid() Use guid(scope, principalId, roleId) for idempotency
11 Access works but condition seems ignored An ABAC condition narrows or the role lacks it Portal IAM → assignment → Condition; --condition in CLI Add/adjust the condition; verify attribute (e.g. tag) matches
12 Global Admin can’t see/manage Azure resources Entra role ≠ Azure RBAC role They have no Owner/Reader assignment Elevate access (tenant root) or grant an Azure role

The expanded reasoning for the four that bite hardest:

1. 403 on data though the user “has Contributor.” Contributor/Owner are control-plane roles; reading a blob, secret, or queue message needs a data-plane role (DataActions). Confirm: az role assignment list --scope <resourceId> --include-inherited lists only Reader/Contributor/Owner, no Storage Blob Data * / Key Vault Secrets *; the error string is typically 403 AuthorizationPermissionMismatch. Fix: assign the correct data role at the resource (or RG) scope — e.g. az role assignment create --role "Storage Blob Data Reader" --scope <saId>. This is the most frequent RBAC support call in existence.

2. Someone has more access than you gave them. An assignment at a parent scope (subscription or management group) inherits down and adds to whatever you set lower. Confirm: --include-inherited on the resource and walk up — the offending Owner/Contributor almost always sits at the subscription or an MG. Fix: remove or narrow the broad parent grant; re-grant at the correct narrower scope. Never try to “subtract” with a more-restrictive role lower down — RBAC is additive, so that does nothing.

6. Even Owner is denied — a deny assignment. A deny assignment (created by Azure Blueprints or a managed application’s deny settings) explicitly blocks operations, and deny always wins over allow. Confirm: Portal → resource → Access control (IAM)Deny assignments tab lists it (you cannot create these yourself in normal RBAC). Fix: address the source — adjust the managed app’s deny settings or the blueprint assignment; you can’t override a deny with a bigger allow role.

7. AuthorizationFailed when creating an assignment. You’re trying to grant access without Microsoft.Authorization/roleAssignments/write — Contributor’s NotActions strip exactly this, which is why a Contributor cannot escalate. Confirm: you hold Contributor (not Owner/UAA/RBAC Admin) at the scope. Fix: get Owner, User Access Administrator, or Role Based Access Control Administrator at that scope; you can only grant access you’re authorised to grant.

Best practices

Security notes

The security-relevant RBAC controls and what each one buys you:

Control Mechanism Protects against
Least-privilege role choice Narrowest built-in / custom role Over-broad blast radius on credential compromise
Narrow scope RG/resource instead of sub/MG One incident reaching the whole estate
PIM eligible roles Just-in-time activation (Entra P2) Standing privileged access being abused
RBAC Administrator over UAA Constrained grant-making Self-escalation to Owner
Activity Log + alerts Monitor roleAssignments/write Silent privilege grants going unnoticed
Access reviews Periodic recertification Stale/forgotten assignments accumulating
ABAC conditions Attribute-scoped grants Data access wider than necessary

Cost & sizing

RBAC has no direct cost: role definitions and assignments are free, at any scope and volume up to the documented limits. Nothing to “size” in the billing sense — the considerations are indirect:

The limits to design against, with the mitigation:

Limit Value (Azure cloud) Mitigation
Role assignments per subscription ~4,000 Assign to groups, not individuals
Role assignments per management group ~500 Scope org-wide roles at MG sparingly
Custom roles per tenant 5,000 Reuse roles across scopes; don’t copy per team
Assignments using a single condition (ABAC) (counts toward the per-sub limit) Use conditions to reduce assignment count
PIM / advanced Access reviews Requires Entra ID P2 License only privileged populations

Interview & exam questions

1. Explain the three components that make up Azure RBAC access. A role definition (a named set of Actions/DataActions minus NotActions/NotDataActions), a scope (management group, subscription, resource group, or resource — inheriting downward), and a role assignment that binds a security principal to that role at that scope. Effective access is the union of all assignments whose scope covers the target resource.

2. A user has Contributor on a storage account but gets 403 reading a blob. Why? Contributor is a control-plane role (Actions); reading blob data needs a data-plane role (DataActions) such as Storage Blob Data Reader. The two planes are separate by design, so managing the account doesn’t grant reading its data. Assign the data role at the account (or RG) scope.

3. Difference between Owner, Contributor and User Access Administrator? Owner has full control including granting access (Microsoft.Authorization/*). Contributor can do everything except grant access — its NotActions block Authorization/*/Write/Delete. User Access Administrator reads everything and manages access but not the resources. For delegating grants, prefer the constrainable Role Based Access Control Administrator.

4. How does scope inheritance work, and how would you grant Owner on a subscription “except one resource group”? An assignment applies to its scope and everything beneath it; no upward inheritance, no per-grant exclusion. You cannot “subtract” an RG from a subscription-level Owner with another assignment (RBAC is additive). Instead, assign the broad role only where you mean it (Owner at each other RG), or use platform-created deny assignments for true exclusions.

5. When should you create a custom role instead of using a built-in? Only when no built-in (or combination) fits — most often when you need fewer permissions than the smallest built-in (e.g. “restart VMs only”), a precise cross-service bundle, or to constrain assignability via AssignableScopes. Clone the nearest built-in and trim it; every custom role is added maintenance and audit surface.

6. What does NotActions do — is it a deny? No. NotActions subtracts operations from the same role’s Actions (effective permission = Actions − NotActions). It is not a global deny — another assignment can still grant what one role’s NotActions removed. True denies come only from Azure-created deny assignments.

7. An assignment looks correct in the portal but the user is still denied. Give three possible causes. (a) Propagation delay — control plane seconds–minutes, data plane longer; (b) a stale token predating the membership/assignment (fix: sign out/in); © a deny assignment or resource lock overriding the allow. Less commonly, an ABAC condition the request doesn’t satisfy.

8. How do you find why a principal has more access than expected? az role assignment list --scope <resource> --include-inherited and walk up the scope tree — the over-grant is almost always an inherited Owner/Contributor at the subscription or a management group. The additive union means a broad parent assignment silently augments everything below.

9. Why assign roles to groups instead of users? Auditability and scale: one assignment covers everyone in the group, membership rides your joiner/mover/leaver process, and you stay well under the ~4,000-assignments-per-subscription limit. Individual assignments become unauditable sprawl past a handful of people.

10. What’s the difference between an Entra ID directory role and an Azure RBAC role? Entra (directory) roles (Global Administrator, User Administrator) manage the directory — users, groups, apps. Azure RBAC roles (Owner, Contributor) manage Azure resources. Separate systems: a Global Admin has no access to Azure resources until they elevate access at the tenant root or receive an Azure role assignment.

11. What is PIM and how does it change RBAC posture? Privileged Identity Management (Entra ID P2) makes privileged roles eligible rather than permanently active — a user activates the role just-in-time with justification, optional approval, and a time limit, then drops back. It shrinks the standing privileged surface so a compromised admin account isn’t continuously Owner.

12. In Bicep, why must a role assignment’s name be a deterministic GUID? The assignment resource’s name is its identity; a stable guid(scope, principalId, roleId) makes re-deploys idempotent (no RoleAssignmentExists conflicts) and maps the same logical assignment to the same resource every time. A random name creates duplicates or fails on re-apply.

These map to AZ-104 (Azure Administrator)manage Azure identities and governance: built-in roles, custom roles, scope, and assignments — and AZ-500 (Security Engineer)manage identity and access: least privilege, PIM, separation of duties. The IaC angle touches AZ-400. A compact cert mapping:

Question theme Primary cert Objective area
Role/scope/assignment model; built-in vs custom AZ-104 Manage identities and governance
Control vs data plane; data roles AZ-104 / AZ-500 Manage access to Azure resources
Least privilege, PIM, separation of duties AZ-500 Manage identity and access
Deny assignments, locks, additive union AZ-104 Governance guardrails
RBAC as Bicep (idempotent assignments) AZ-400 Infrastructure as code

Quick check

  1. Name the three things that together make up a role assignment, and state how Azure combines multiple assignments into effective access.
  2. A user is Contributor on a Key Vault but gets 403 reading a secret. What’s wrong and what role fixes it?
  3. True or false: assigning Reader to a resource group “cancels out” a subscription-level Contributor for that group’s resources.
  4. You created a custom role but it won’t assign at a particular resource group. What’s the most likely cause?
  5. A user was just added to the correct group but is still denied. What’s the quickest thing to try?

Answers

  1. A security principal (who), a role definition (what), and a scope (where). Azure combines all assignments covering a resource as their union — access is additive, with no author-able deny (only Azure-created deny assignments override).
  2. Contributor is a control-plane role; reading a secret needs a data-plane role. In a Key Vault using the RBAC permission model, assign Key Vault Secrets User (or Key Vault Administrator for full data-plane) at the vault scope.
  3. False. RBAC is additive — the Contributor still applies (the union of Contributor + Reader is Contributor). To restrict, you must not assign the broad role at the higher scope in the first place.
  4. The resource group’s scope (or a parent of it) is not in the custom role’s AssignableScopes. Add it with az role definition update. A custom role can only be assigned within its declared assignable scopes.
  5. Have them sign out and back in (or refresh their token). Their current access token was issued before the group membership and doesn’t carry the new group claim; a fresh token resolves it.

Glossary

Next steps

You can now model, assign and troubleshoot RBAC across every scope. Build outward:

AzureRBACIAMEntra IDLeast PrivilegeRole AssignmentGovernanceAZ-104
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading