Security

How to Implement RBAC: A Hands-On Guide With an Azure Example

Intermediate20 min to complete6 min readSeptember 17, 2026

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.

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
RBAC
Azure
IAM
Access Control
Security
Cloud Engineering

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 Actions and DataActions
  • 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.

bash
az role definition list --name "Reader" --query "[0]" -o json

You'll get back something like this (trimmed):

json
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:

bash
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:

bash
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:

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:

  • Actions cover the Azure Resource Manager control plane — creating, reading, and managing resources themselves.
  • DataActions cover 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.
  • AssignableScopes is not optional decoration — it's the hard ceiling on where this role can ever be assigned. Replace the placeholder subscription ID with your own:
bash
SUBSCRIPTION_ID=$(az account show --query id -o tsv)
sed -i.bak "s/00000000-0000-0000-0000-000000000000/$SUBSCRIPTION_ID/" custom-role.json

Create the role:

bash
az role definition create --role-definition custom-role.json

Confirm it exists:

bash
az role definition list --custom-role-only true --query "[].roleName" -o tsv

Step 4: Assign the Custom Role

Bind the role to a principal at resource-group scope — the same $SCOPE from Step 2:

bash
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:

bash
az role assignment create \
  --assignee "11111111-2222-3333-4444-555555555555" \
  --role "App Deployer" \
  --scope "$SCOPE"

Step 5: Verify

bash
az role assignment list --assignee "[email protected]" --all -o table

You should see App Deployer listed with the resource group's scope. To check from the other direction — everyone with access to a given scope:

bash
az role assignment list --scope "$SCOPE" -o table

If 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?

bash
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

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.