OnCallReady

Lesson 22.13 · Azure I: CLI, Identity & Data Planes · 15 min read

Azure RBAC: roles, scopes, inheritance

In plain words

Imagine a key-card system in a big building. Every rule on the system has exactly three parts: which person, which kind of access (open doors, or also change the locks), and which part of the building (the whole building, one floor, one room). A card that opens a floor opens every room on that floor. And rules only ever add access: there's no rule that says "except this room" for normal people.

A role assignment in Azure is exactly those three parts: a principal's object ID, a role definition, and a scope like /subscriptions/.../resourceGroups/rg-shop-prod. It applies at its scope and below, never up or sideways. Look at the roles themselves and you'll see why Owner can't read a Key Vault secret: Owner has control-plane actions, but no dataActions.

You are Owner of the subscription, yet reading a secret fails with Forbidden. A pipeline has "no access" according to one command and deletes a resource group the next day. Both are the same question: who may do what, where - and how to read the answer correctly.

What you need to know already: 22.1 (the scope hierarchy, resource IDs, control vs data plane), 22.8 (principals, object IDs), 22.10 (your first role assignment), 17.30 (Kubernetes RBAC: the same idea inside a cluster).

Azure RBAC (role-based access control) is how Azure decides what a signed-in principal may do with resources. A role assignment is three things, and only three:

WHO     a principal (user, group, service principal, managed identity) - by object id
WHAT    a role definition - a list of allowed actions
WHERE   a scope - management group, subscription, resource group or resource

Everything else is detail on one of those three. (Kubernetes RoleBindings in 17.30 have the same shape: subject, role, namespace.)

Role definitions

A role definition (a role) is a named list of permissions. az role definition list --name "<role>" shows one; --query "[0].permissions" keeps just its permission lists.

$ az role definition list --name "Key Vault Secrets User" --query "[0].permissions"
[
  {
    "actions": [],
    "condition": null,
    "conditionVersion": null,
    "dataActions": [
      "Microsoft.KeyVault/vaults/secrets/getSecret/action",
      "Microsoft.KeyVault/vaults/secrets/readMetadata/action"
    ],
    "notActions": [],
    "notDataActions": []
  }
]

Each permission is an operation string: provider / resource type / verb. Microsoft.KeyVault/vaults/secrets/getSecret/action = "get the value of a secret in a Key Vault".

Wildcards are allowed: *, */read, Microsoft.KeyVault/vaults/secrets/*.

So this role has no control-plane actions at all, and two data actions: read a secret's value, read its metadata (name, dates - not the value).

The built-in roles you actually use

Azure ships built-in roles; you can also write your own (lesson 22.18).

roleactionsdataActionsmeans
Owner*-everything on the control plane, including granting access
Contributor* minus Microsoft.Authorization/*/Write, /Delete-build anything, cannot grant access
Reader*/read-see everything, change nothing
User Access Administrator*/read, Microsoft.Authorization/*-grant access, cannot build
Key Vault Secrets User-getSecret, readMetadataread secret values
Key Vault Secrets Officerread vaultsecrets/*manage secrets
Key Vault Readerread vault*/read, readMetadatametadata only - not values
Storage Blob Data Reader-blobs/readread blobs
Storage Blob Data Contributorcontainers/*blobs read/write/deleteread and write blobs
AcrPullregistries/pull/read-pull images

(Microsoft.Authorization/* is the permission to create and delete role assignments themselves.)

Look at the dataActions column of Owner and Contributor: empty. That is why the subscription Owner gets ForbiddenByRbac reading a Key Vault secret. It is not a bug; it is the design. You can manage the vault, and you can grant yourself the data role - which leaves an audit trail (a record of who did what).

Scope and inheritance

/                                            (root management group)
└─ /subscriptions/0000...4444                Owner for learner
   └─ /resourceGroups/rg-shop-prod           Contributor + UAA for sp-shop-ci
      └─ /providers/Microsoft.KeyVault/vaults/kv-shop-prod     Key Vault Reader for id-orders
         └─ /secrets/orders-db-password      (Key Vault allows scope down to one secret)

(UAA = User Access Administrator.) An assignment applies at its scope and everything below it - that is inheritance. Never upwards, never sideways. So:

Permissions are additive: the effective permission (what you can really do) is the union of every assignment at the scope and above. There is no "more specific wins", and a Reader assignment on a resource does not reduce an Owner inherited from above.

The only subtraction is a deny assignment: an explicit "no" that beats any allow. You cannot create one yourself - Azure creates them for a few managed features (for example to stop you editing resources another service owns).

Listing assignments - where people get it wrong

az role assignment list                          # ONLY the subscription scope
az role assignment list --all                    # every scope in the subscription
az role assignment list --scope <id>             # exactly that scope
az role assignment list --scope <id> --include-inherited    # that scope and above
az role assignment list --assignee <upn|appId|objectId> --all

The default is the trap: without --all or --scope, the CLI shows only assignments made at the subscription. Resource group and resource assignments are invisible, and "they have no access" is the wrong conclusion.

$ az role assignment list --scope $(az group show -n rg-shop-prod --query id -o tsv) -o table
Principal                             Role                       Scope
------------------------------------  -------------------------  --------------------------------------
4802bf9d-54aa-4df7-b396-7a1eeaae0e8c  Contributor                /subscriptions/.../resourceGroups/rg-shop-prod
4802bf9d-54aa-4df7-b396-7a1eeaae0e8c  User Access Administrator  /subscriptions/.../resourceGroups/rg-shop-prod
0b8f8316-8e8c-49bb-987e-bfda3fd12801  AcrPull                    /subscriptions/.../resourceGroups/rg-shop-prod

Three grants made exactly on the group. Your Owner is not in that list (it is inherited, add --include-inherited), and neither is the vault-scoped Key Vault Reader (it is below this scope).

The Principal column shows a UPN for users and the appId for service principals and managed identities - the RBAC entry itself holds the object id (principalId in the JSON). An empty Principal means the principal was deleted: an orphan (22.11).

Who granted it, and when

az role assignment list --scope "$KV" --query "[].{role:roleDefinitionName, by:createdBy, on:createdOn}" -o table

createdBy is an object id; az ad sp show --id <it> or az ad user show --id <it> turns it into a name. The Activity Log - Azure's record of every control-plane write in a subscription - keeps the role-assignment writes (MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE) for 90 days.

Later (Ch 24): a Log Analytics workspace can keep the Activity Log for as long as you pay for, and you query it with KQL.

Propagation

A new assignment is not instant. ARM caches authorisation decisions, and a client may hold a token issued before the change. Expect up to 10 minutes, occasionally more, and design for it: pre-create identities and their assignments (user-assigned identities make that possible), and do not treat the first 403 after a grant as proof it did not work. az logout and az login refreshes your own token when you are the one waiting.

Grant carefully

What you can now do

Why it helps

RBAC is behind most Azure tickets you'll get: "the pipeline can't deploy", "the pod gets 403", "who gave this intern Owner?". Knowing that assignments are additive, inherited downward and listed only at subscription scope by default lets you answer them correctly instead of concluding "they have no access" from a listing that hid half the picture.

It also matters in PR reviews of Terraform. An azurerm_role_assignment with Contributor at subscription scope for an app that only reads blobs is a finding. A pipeline holding User Access Administrator can make itself Owner. And first-403-after-a-grant is normal propagation, not a broken assignment. These are also staple interview questions for Azure platform roles, including the Owner and Key Vault one.

Commands in this lesson

az

FAQ

Does a more specific role override a broader one?

No. Azure RBAC is additive: your effective permissions are the union of every assignment at the scope and above, for you and every group you're in. A Reader assignment on a resource doesn't reduce an Owner assignment inherited from the subscription. The only subtraction is a deny assignment, which you can't create directly; Azure creates them for managed applications and deployment stacks, and they beat any allow.

What's the difference between actions and dataActions?

actions are control-plane operations through Azure Resource Manager, like Microsoft.Network/virtualNetworks/write or creating a vault. dataActions are operations on the data inside a service, like reading a secret's value, writing a blob or listing pods through a cluster's Kubernetes API with Azure RBAC. Owner and Contributor have wide actions and no dataActions, which is why you need roles like Key Vault Secrets User or Storage Blob Data Reader for data.

Why doesn't az role assignment list show an assignment I know exists?

Because without arguments it only lists assignments made at the subscription scope. Assignments on resource groups and resources are invisible, and so are inherited ones from management groups. Use --all for every scope in the subscription, --scope <id> for exactly one scope, --include-inherited to add scopes above it, and --assignee <id> --all --include-groups for everything one principal has, including through groups.

Why does access take a while to work after I grant it?

Two reasons. ARM caches authorisation decisions, and a client may be using a token issued before the change, which doesn't contain new group memberships either. Expect up to about 10 minutes, occasionally more. So don't treat the first 403 after a grant as proof it failed. For your own CLI session, az logout and az login refreshes the token. For workloads, pre-create identities and their assignments so access exists before the first start.

Why is User Access Administrator considered so powerful?

Because it grants Microsoft.Authorization/*, the right to create role assignments, and anyone who can assign roles can assign themselves Owner. So a pipeline holding it effectively holds Owner. The safer option is Role Based Access Control Administrator with a condition that restricts which roles it may assign, for example only Key Vault Secrets User and AcrPull, so Terraform can grant app access without being able to escalate itself.

In an interview Mid

What are the parts of an Azure role assignment, and how do you work out what someone can actually do?

Three parts: who (a principal's object id), what (a role definition), where (a scope: management group, subscription, resource group, resource).

A role definition lists actions (control plane, ARM) and dataActions (data inside a service: getSecret, blob read). notActions only subtract within that role - not a deny. Owner and Contributor have no dataActions; Contributor also cannot write Microsoft.Authorization.

Effective access = the union of every assignment at the scope and above (inheritance flows down only). Nothing "more specific wins"; only a deny assignment subtracts.

Checking it without the classic trap:

Least privilege: data roles for data, smallest scope, Terraform owns the assignments.

Also asked: Why can a subscription Owner not read secrets from a Key Vault? · Why is User Access Administrator considered as powerful as Owner? · How do you implement least privilege with Azure RBAC?

Practise this lesson in the terminal Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.