Azure RBAC

Assignments, scope, least privilege, two planes.

Beginner30 min · lesson 2 of 15

An Azure subscription runs a lot like a hotel. Microsoft Entra ID, the identity directory you met in the previous lesson, is the front desk: it checks your passport and hands you a keycard. That is *authentication*, proving who you are. Azure RBAC (role-based access control) is the keycard system behind the doors. It decides which doors your card opens. That is *authorization*, deciding what you are allowed to do. Programming a card takes three answers: whose card is it, what kind of doors should it open, and on which floors does it work. Azure names those three the security principal, the role definition, and the scope. The programmed card itself, the thing that actually exists in your subscription, is a role assignment.

Hold those two systems apart in your head, because the AZ-104 exam keeps probing the seam between them. Entra ID roles such as Global Administrator and User Administrator govern the *directory*: users, groups, app registrations. Azure RBAC governs *resources*: virtual machines (VMs), storage accounts, networks. A Global Administrator can reset every password in the tenant (your organization's own instance of the directory) and still be unable to read a single blob, which is Azure's word for a file sitting in storage. A subscription Owner can delete every VM you have and still be unable to create one user account. Plenty of people need both. Each grant gets made deliberately, in its own system.

What a role assignment is actually made of

A role assignment ties a security principal to a role definition, at a scope. The principal is a user, a group, a service principal (an identity that belongs to an application rather than a person), or a managed identity (an identity Azure creates and rotates for a resource so nobody has to store a password). Azure ships hundreds of built-in role definitions, and four generic ones hold up the whole model. Reader sees everything and changes nothing. Contributor manages everything *except* access. Owner manages everything *including* access. User Access Administrator manages access and nothing else. The line between Contributor and Owner is classic exam material. A Contributor can drop a production database, yet cannot give anybody else permission to do the same. Only a role carrying Microsoft.Authorization/roleAssignments/write can hand out keys: Owner, User Access Administrator, or the tighter Role Based Access Control Administrator. Those are the assignments you audit hardest.

assign at the narrowest scope
# Grant a group (never an individual) a resource-specific role, scoped to ONE resource group
az role assignment create \
--assignee-object-id "1f4b2c8e-93a1-4c7d-b1e6-2a9f0d3c5e77" \
--assignee-principal-type Group \
--role "Virtual Machine Contributor" \
--scope "/subscriptions/0b1f6471-1bf0-4dda-aec3-cb9272f09590/resourceGroups/rg-app-prod"
# Expected output (trimmed):
# {
# "principalId": "1f4b2c8e-93a1-4c7d-b1e6-2a9f0d3c5e77",
# "principalType": "Group",
# "roleDefinitionId": ".../roleDefinitions/9980e02c-c2be-4d73-94e8-173b1dc7cf3c",
# "scope": "/subscriptions/0b1f6471-.../resourceGroups/rg-app-prod",
# "type": "Microsoft.Authorization/roleAssignments"
# }

Three details in that command earn their keep. Passing --assignee-object-id skips the Microsoft Graph lookup that a plain --assignee performs for you, so the command still works when the caller has no permission to search the directory, and pairing it with --assignee-principal-type keeps scripts from tripping over Graph replication lag, the short window where a brand-new object exists in one place but has not copied to another yet. The assignee is a *group*, which turns joiners and leavers into membership changes instead of RBAC changes. And the scope is one resource group, not the whole subscription. Expect a wait before the grant bites. Azure Resource Manager caches authorization data, so a fresh assignment can take up to ten minutes to take effect. Do not burn an afternoon debugging a 403 (access denied) that you created ninety seconds ago.

Scope, inheritance, and what Azure does with your request

Scope is a path down the resource hierarchy, the way a folder path runs down a drive: management group → subscription → resource group → resource. Grant something at any level and everything below it inherits the grant. Reader at a management group means Reader on every resource in every subscription underneath. When a request lands at Azure Resource Manager (ARM, the single front door every Azure API call goes through), the evaluation is mechanical. ARM reads the principal out of your token. It gathers every role assignment and every deny assignment that could apply, whether you got it directly or through a group, at the target scope or anywhere above it. Deny assignments get checked first. If none match, ARM takes each role's Actions, subtracts that same role's NotActions, and unions the leftovers across every role you hold. Two consequences fall out of that. RBAC is additive: no assignment can cancel another, and NotActions only trims the role it lives in, so a second role can quietly hand back the action the first one took away. The one true "no" is a deny assignment, created by deployment stacks and Azure managed applications. (Azure Blueprints created them too and is being retired in favor of deployment stacks: phased retirement starts July 31, 2026, and final retirement lands January 31, 2027.) One matching deny beats any number of allows.

Two planes of Azure access
Directory plane: Microsoft Entra ID
Governs users, groups, apps
authentication + directory objects
Global Administrator
can reset every password in the tenant
Cannot read a single blob
no authority over resources
Resource plane: Azure RBAC
Governs VMs, storage, networks
authorization on resources
Subscription Owner
can delete every VM
Cannot create a user
no authority over the directory
Kept apart on purpose, because AZ-104 tests the boundary. There is one designed bridge: a Global Administrator can flip 'Access management for Azure resources' and grant themselves User Access Administrator at root scope (/), which reaches across both planes.

Control plane and data plane: the box, and what is inside it

A locked filing cabinet raises two separate questions. Who owns the cabinet, and who gets to read the folders inside it? Azure answers them with different roles, which is why any role definition you open has four permission lists rather than two. Actions and NotActions cover the control plane, meaning what you can do to a resource as an object: create it, delete it, read its configuration, rotate its keys. DataActions and NotDataActions cover the data plane, meaning what you can do to the contents: the blobs in a container, the messages in a queue, the secrets in a key vault. This catches people constantly. Reader on a storage account lets you *see* the account in the portal and will not let you read one blob with your Entra sign-in, because Reader has no DataActions at all. Reading blobs needs Storage Blob Data Reader. Microsoft keeps steering storage away from shared account keys and toward Entra sign-ins, so data-plane roles now decide who reaches real data. The storage lessons build straight on this.

inspect a role definition
az role definition list --name "Storage Blob Data Reader" --query "[0].permissions[0]"
# {
# "actions": [
# "Microsoft.Storage/storageAccounts/blobServices/containers/read",
# "Microsoft.Storage/storageAccounts/blobServices/generateUserDelegationKey/action"
# ],
# "dataActions": [
# "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read"
# ],
# "notActions": [],
# "notDataActions": []
# }
# Control plane: see containers. Data plane: read the blobs themselves.

When no built-in role fits

Reach for a custom role only after the closest built-in has genuinely failed you. Every custom role you create is yours to document, review, and maintain for as long as it exists. A custom role is a JSON (JavaScript Object Notation, the plain-text format Azure reads definitions in) document: the same four permission lists plus AssignableScopes, which fences off where the role is allowed to be assigned at all. The classic case is an operations team that must start, stop, and restart VMs but must never create, delete, or resize them. No built-in role draws that exact line. Two limits are worth memorizing for the exam. A tenant holds at most 5,000 custom roles, and a custom role containing DataActions cannot be assigned at management-group scope.

vm-operator custom role
cat > vm-operator.json <<'EOF'
{
"Name": "VM Operator",
"Description": "Start, stop, restart VMs. Cannot create, delete, or reconfigure.",
"Actions": [
"Microsoft.Compute/virtualMachines/read",
"Microsoft.Compute/virtualMachines/start/action",
"Microsoft.Compute/virtualMachines/restart/action",
"Microsoft.Compute/virtualMachines/deallocate/action"
],
"NotActions": [],
"AssignableScopes": ["/subscriptions/0b1f6471-1bf0-4dda-aec3-cb9272f09590"]
}
EOF
az role definition create --role-definition @vm-operator.json
# {
# "roleName": "VM Operator",
# "roleType": "CustomRole",
# "assignableScopes": ["/subscriptions/0b1f6471-..."],
# ...
# }
# Note: deallocate (releases compute, stops billing) — not powerOff (billing continues).

Keeping access small, and auditing that it stayed small

Least privilege survives production as a set of habits rather than a slogan. Assign roles to groups, not to individuals. Pick the narrowest scope that still gets the work done. Prefer a resource-specific built-in like Virtual Machine Contributor or Network Contributor over blanket Contributor. Keep standing Owner assignments as close to zero as you can, and make privileged access time-bound through Privileged Identity Management (PIM), which lends someone a role for a few hours and takes it back on its own. Two more items belong on the audit checklist. Each subscription tops out at 4,000 role assignments, and past that ceiling new assignments fail outright, so sprawl turns into an outage rather than an eyesore. And any Global Administrator can flip the *Access management for Azure resources* toggle to grant themselves User Access Administrator at root scope (/), covering every subscription in the tenant at once. That is the designed break-glass bridge between the two planes. It shows up on the exam, and in real life it should page somebody the moment it happens.

three audit queries to run monthly
# 1. Everything one person can do, including via groups and inherited scopes
az role assignment list --assignee [email protected] \
--all --include-groups --include-inherited \
--query "[].{role:roleDefinitionName, scope:scope}" -o table
# Role Scope
# --------------------------- -----------------------------------------------
# Reader /subscriptions/0b1f6471-...
# Virtual Machine Contributor /subscriptions/0b1f6471-.../resourceGroups/rg-app-prod
# 2. Who can grant access to others (the escalation-capable roles)
az role assignment list --all \
--query "[?roleDefinitionName=='Owner' || roleDefinitionName=='User Access Administrator' || roleDefinitionName=='Role Based Access Control Administrator'].{who:principalName, scope:scope}" -o table
# 3. Orphaned assignments pointing at deleted principals
az role assignment list --all \
--query "[?principalName==''].{principalId:principalId, role:roleDefinitionName, scope:scope}" -o table
Deleting the account does not delete its access
Deleting a user, a group, or a service principal does not delete the role assignments attached to it. They hang around as orphans, showing up as *Identity not found* in the portal, and they still count against the 4,000-per-subscription limit. Here is the sharp edge. Recreating a service principal under the same name restores nothing, because the new object gets a brand-new principalId while the old assignment keeps pointing at the dead one. Sweep for orphans with query 3 above, and favor group assignments so staff churn moves through group membership instead of through RBAC.

RBAC answers one question: *who can do what, where*. That is the whole of its job. Nothing in RBAC stops a perfectly authorized Contributor from deploying an untagged, oversized VM in a region your company never approved. Nothing stops an Owner from deleting a critical resource in one fat-fingered moment. Limiting what is allowed to *exist*, no matter who is asking, belongs to a different layer of control: Azure Policy for the rules, resource locks for tamper-proofing, tags for accountability. That governance layer is exactly where the next lesson picks up.

Scope is the part people rush past. The same Contributor role at a management group can rebuild every subscription underneath it. That same role at a single storage account can touch one resource and nothing else. So when somebody asks "why can they delete my VM?", run az role assignment list at the resource first, then at the resource group, then at the subscription. Inheritance means the permission is often sitting a level or two above the object you are staring at.

Built-in roles cover most day-to-day work: Reader, Contributor, Owner, and the service-specific ones like Storage Blob Data Contributor over on the data plane. Custom roles exist for the gaps, and each one is a maintenance tax you pay forever. Stay on built-ins until you can write out the exact Actions and NotActions you need. And keep remembering that data-plane roles (Key Vault, Storage, AKS or Azure Kubernetes Service) sit apart from the management-plane Contributor you already know.

Try this

Pick a resource group you can afford to break in a lab subscription. Give yourself Reader on the group, try a write, and watch it fail. Then raise yourself to Contributor on that one group and confirm the same write goes through.

terminal
RG=rg-lab-rbac
az group create -n $RG -l eastus
# Least privilege: Reader at the RG
az role assignment create \
--assignee $(az account show --query user.name -o tsv) \
--role Reader \
--scope $(az group show -n $RG --query id -o tsv)
az role assignment list --scope $(az group show -n $RG --query id -o tsv) -o table
output
$ az role assignment list --scope /subscriptions/.../resourceGroups/rg-lab-rbac -o table
Principal Role Scope
------------------------ ------- ----------------------------------
[email protected] Reader /subscriptions/.../rg-lab-rbac
# Sample output — write blocked while Reader:
# AuthorizationFailed: does not have authorization to perform action 'Microsoft.Storage/...'

Takeaway

An RBAC assignment is always three parts: who (the principal), what (the role), and where (the scope). Permissions flow downhill through the hierarchy, and a deny assignment beats any allow above it.

Keep practising the two planes: Entra ID roles for the directory, Azure RBAC for resources. And when Contributor on one resource group does the job, never reach for Owner across the whole subscription.

Quick check
01A user sits in two groups, and both groups are assigned roles at the same subscription scope. Group A has a custom role with broad Contributor-style access, but it lists Microsoft.Compute/virtualMachines/delete in its NotActions. Group B has the built-in Virtual Machine Contributor role. Can that user delete a VM at that scope?
Correct — RBAC is additive. NotActions subtracts inside the role that declares it, no assignment can cancel another, and a second role can hand back what the first one removed.
Incorrect — No. NotActions subtracts inside its own role. It is not a deny that reaches across roles, so it cannot suppress an action another role grants.
Incorrect — No. RBAC has no conflict-deny rule. Nothing cancels an assignment, so the allow coming from Group B stands.
Incorrect — No. Deny assignments only ever block, never grant. None is needed here, and one would stop the delete outright.
02An engineer is granted the built-in Reader role on a storage account and signs in with their Microsoft Entra ID credentials. They can see the account in the portal, but they cannot read the contents of any blob. What is going on?
Correct — A role definition splits permissions into control plane (Actions and NotActions) and data plane (DataActions and NotDataActions), and Reader has no DataActions at all.
Incorrect — No. Reader has no DataActions to strip in the first place, and no NotAction is involved here.
Incorrect — No. Locks apply to control-plane management operations, not to data-plane blob reads, so a lock would not cause this.
Incorrect — No. Entra credentials read blobs perfectly well once you hold the right data-plane role, which is the direction Microsoft keeps pushing storage.
03An operations team needs to start, stop (deallocate), and restart virtual machines, and must never be able to create, delete, or resize them. No single built-in role draws exactly that line. What is the BEST way to grant this?
Incorrect — No. Contributor can create, delete, and resize VMs, which is far past what this team should be able to do.
Incorrect — No. Virtual Machine Contributor can create and delete VMs too, so it overshoots the boundary you were asked for.
Correct — A custom role is the tool for a permission set no built-in matches, granting those four actions and nothing beyond them.
Incorrect — No. Reader carries no action permissions at all, so the team could not start, stop, or restart anything.

Related