How to Implement RBAC: A Hands-On Guide With an Azure Example
Quick answer
Role-based access control isn't a Kubernetes-only concept — every cloud has its own flavor. Here's how Azure RBAC actually works, built from the ground up with a custom role, a scoped assignment, and the CLI commands to verify both.
- Step 1: Look at a Built-In Role First
- Step 2: Understand the Scope Hierarchy
- Step 3: Write a Custom Role Definition
- Step 4: Assign the Custom Role
- Step 5: Verify
intermediate · 20 min
Before you begin
- An Azure subscription where you have Owner or User Access Administrator role
- Azure CLI installed and authenticated (`az login`)
- Basic comfort with JSON and the command line
Every cloud platform reinvents the same idea: give a principal the smallest set of permissions it actually needs, at the smallest scope that still lets it do its job. Kubernetes calls this RBAC. AWS calls it IAM policies. Azure calls it Azure RBAC — and if you've only ever touched the Kubernetes version, the Azure model will feel familiar in spirit but different in every detail.
If you're looking for Kubernetes RBAC specifically, see Setting Up Kubernetes RBAC from Scratch instead — this tutorial is about controlling access to Azure resources themselves: who can read a storage account, restart a VM, or delete a resource group.
Azure RBAC has four moving parts, and the whole system is just combinations of these:
- Security principal — a user, group, service principal, or managed identity. The "who."
- Role definition — a named collection of permissions (
Actions,NotActions,DataActions,NotDataActions). The "what." - Scope — a management group, subscription, resource group, or single resource. The "where."
- Role assignment — the binding that ties a principal, a role, and a scope together. Nothing has access until this exists.
By the end of this tutorial you'll have created a custom role from scratch, assigned it at resource-group scope, and verified the assignment with the CLI.
What You'll Build
- A working understanding of Azure's scope hierarchy and how permissions inherit downward
- A custom role definition, written as JSON, with precisely scoped
ActionsandDataActions - A role assignment binding that custom role to a principal at resource-group scope
- A verification pass using
az role assignment list - Awareness of where Privileged Identity Management (PIM) fits in once static assignments aren't enough
Step 1: Look at a Built-In Role First
Before writing a custom role, it's worth seeing what a built-in one actually looks like — this is the shape you're about to copy.
az role definition list --name "Reader" --query "[0]" -o jsonYou'll get back something like this (trimmed):
1{
2 "roleName": "Reader",
3 "description": "View all resources, but does not allow you to make any changes.",
4 "assignableScopes": ["/"],
5 "permissions": [
6 {
7 "actions": ["*/read"],
8 "notActions": [],
9 "dataActions": [],
10 "notDataActions": []
11 }
12 ]
13}assignableScopes: ["/"] means this role can be assigned anywhere in the tenant — that's normal for Microsoft's built-in roles, and exactly what you don't want for a custom role you're about to scope tightly.
Step 2: Understand the Scope Hierarchy
Azure RBAC scopes nest, and permissions inherit downward:
Management Group
└── Subscription
└── Resource Group
└── Resource
A role assigned at the subscription level applies to every resource group and resource beneath it. Assign at the narrowest scope that actually satisfies the requirement — a role meant for one application's resources belongs at the resource-group level, not the subscription.
Every scope has a concrete path. For a resource group:
SUBSCRIPTION_ID=$(az account show --query id -o tsv)
RESOURCE_GROUP="app-prod-rg"
SCOPE="/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP"
echo "$SCOPE"You'll reuse $SCOPE in the assignment step below. A built-in role can be assigned at that scope right now, with no custom work at all:
az role assignment create \
--assignee "[email protected]" \
--role "Reader" \
--scope "$SCOPE"That's often enough. Reach for a custom role when the built-in roles are either too broad (Contributor can do almost anything) or don't exist for the exact slice of permissions you need.
Step 3: Write a Custom Role Definition
A custom role is a JSON document. Create custom-role.json:
1{
2 "Name": "App Deployer",
3 "IsCustom": true,
4 "Description": "Can deploy and restart web apps, but cannot delete resources or read secrets.",
5 "Actions": [
6 "Microsoft.Web/sites/read",
7 "Microsoft.Web/sites/restart/action",
8 "Microsoft.Web/sites/publishxml/Action",
9 "Microsoft.Resources/deployments/*"
10 ],
11 "NotActions": [],
12 "DataActions": [
13 "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read"
14 ],
15 "NotDataActions": [],
16 "AssignableScopes": [
17 "/subscriptions/00000000-0000-0000-0000-000000000000"
18 ]
19}A few things worth being deliberate about:
Actionscover the Azure Resource Manager control plane — creating, reading, and managing resources themselves.DataActionscover operations inside a resource's data plane — reading a blob's contents, not just knowing the storage account exists. These are separate permission surfaces on purpose; a principal can manage a storage account without ever being able to read what's in it.AssignableScopesis not optional decoration — it's the hard ceiling on where this role can ever be assigned. Replace the placeholder subscription ID with your own:
SUBSCRIPTION_ID=$(az account show --query id -o tsv)
sed -i.bak "s/00000000-0000-0000-0000-000000000000/$SUBSCRIPTION_ID/" custom-role.jsonCreate the role:
az role definition create --role-definition custom-role.jsonConfirm it exists:
az role definition list --custom-role-only true --query "[].roleName" -o tsvStep 4: Assign the Custom Role
Bind the role to a principal at resource-group scope — the same $SCOPE from Step 2:
az role assignment create \
--assignee "[email protected]" \
--role "App Deployer" \
--scope "$SCOPE"For a service principal or managed identity, --assignee takes the object ID or application (client) ID instead of a UPN:
az role assignment create \
--assignee "11111111-2222-3333-4444-555555555555" \
--role "App Deployer" \
--scope "$SCOPE"Step 5: Verify
az role assignment list --assignee "[email protected]" --all -o tableYou should see App Deployer listed with the resource group's scope. To check from the other direction — everyone with access to a given scope:
az role assignment list --scope "$SCOPE" -o tableIf the assignee doesn't show up yet, see the propagation note below before assuming something's broken.
Step 6: Where PIM Fits (Next Step, Not This Tutorial)
Everything above creates a standing assignment — the principal has that access permanently, the moment the assignment exists. For high-privilege roles (Owner, User Access Administrator, anything touching production data), Microsoft Entra Privileged Identity Management (PIM) turns that into a just-in-time elevation instead: the principal is eligible for the role but has to explicitly activate it, usually with an approval step and a time limit, and the activation is logged. Worth adopting once you have more than a couple of people needing occasional elevated access — it's a separate setup pass on top of what you built here, not a replacement for it.
Common Issues
Assignment doesn't seem to take effect immediately. Azure RBAC assignments can take a few minutes to propagate globally. If az role assignment list already shows it but the principal still gets denied, wait five minutes before debugging further.
Assigned at too broad a scope by mistake. It's easy to az role assignment create at the subscription level out of habit and grant far more than intended. Always echo $SCOPE before running the command, and audit periodically with az role assignment list --scope /subscriptions/$SUBSCRIPTION_ID -o table to catch assignments that crept upward.
Confusing Actions and DataActions. Granting Microsoft.Storage/storageAccounts/* in Actions lets a principal manage a storage account's configuration — it does not let it read blob contents. That requires a separate DataActions entry. The two permission planes are independent by design; don't assume one implies the other.
Reaching for Owner or Contributor instead of a scoped role. Both are enormous built-in roles meant for administrators, not for a deployment pipeline or a single application. If you find yourself assigning Contributor "just to get it working," that's the signal to write a custom role instead.
Frequently Asked Questions
How is Azure RBAC different from Conditional Access?
Azure RBAC controls what an authenticated principal can do once it has access — read a resource, restart a VM, and so on. Conditional Access, part of Microsoft Entra ID, controls whether and how a user can sign in at all — requiring MFA from an untrusted network, blocking legacy authentication, restricting sign-in to managed devices. They're complementary layers: Conditional Access gates the front door, RBAC governs what happens once someone's inside.
Can a custom role be assigned across multiple subscriptions?
Yes, if AssignableScopes includes more than one subscription (or a management group that contains them). List several subscription IDs in the array, or point at the management group directly. The role assignment itself still has to be created separately at each scope you want it active in — defining the role somewhere broad doesn't assign it anywhere automatically.
How do I remove a role assignment?
az role assignment delete \
--assignee "[email protected]" \
--role "App Deployer" \
--scope "$SCOPE"Deleting the assignment doesn't delete the role definition — the custom role still exists and can be reassigned elsewhere. To delete the role definition itself (only possible once no assignments reference it), use az role definition delete --name "App Deployer".
Does this apply to "classic" (ASM) resources?
No. Azure RBAC governs resources deployed through Azure Resource Manager (ARM) — which is effectively everything created today. The old classic deployment model (ASM) predates RBAC and used a separate co-administrator system. If you're not sure which model a resource uses, it's almost certainly ARM; classic resources have been deprecated for years and are rare outside long-lived legacy subscriptions.
Official References
- Azure RBAC overview — the full permission model, scopes, and how evaluation works
- Azure custom roles — the complete role-definition JSON schema
- Assign Azure roles using the Azure CLI — every
az role assignmentflag and pattern - What is Microsoft Entra Privileged Identity Management — just-in-time elevation for high-privilege roles
We built Podscape to simplify Kubernetes workflows like this — logs, events, and cluster state in one interface, without switching tools.
Struggling with this in production?
We help teams fix these exact issues. Our engineers have deployed these patterns across production environments at scale.