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".
- actions: control-plane operations on ARM (
Microsoft.Network/virtualNetworks/write). - notActions: subtracted from actions. Not a deny - another role can still grant them.
- dataActions: operations on the data inside a service (read a secret, write a blob, list pods through the Kubernetes API with Azure RBAC).
- notDataActions: subtracted from dataActions.
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).
| role | actions | dataActions | means |
|---|---|---|---|
| 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, readMetadata | read secret values |
| Key Vault Secrets Officer | read vault | secrets/* | manage secrets |
| Key Vault Reader | read vault | */read, readMetadata | metadata only - not values |
| Storage Blob Data Reader | - | blobs/read | read blobs |
| Storage Blob Data Contributor | containers/* | blobs read/write/delete | read and write blobs |
| AcrPull | registries/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:
- learner (Owner at the subscription) can manage
kv-shop-prod- inherited. - sp-shop-ci can grant access on
kv-shop-prod- UAA inherited from the group. - id-orders can read vault metadata, and nothing else in the group.
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
- Least privilege: the smallest scope and the smallest role that works. A resource beats a group beats a subscription.
- Data roles for data. Do not give Contributor to an app that needs to read blobs; give Storage Blob Data Reader on one account.
- Nobody standing Owner in prod. PIM eligible assignments (lesson 22.18), activated with a reason for a time window.
- Terraform owns assignments (
azurerm_role_assignment), next to the thing they grant access to, withprincipal_typeset. Portal-made assignments are invisible drift (13.21). - User Access Administrator is powerful: whoever holds it can make themselves Owner. So can a pipeline that holds it. The newer Role Based Access Control Administrator role supports conditions (extra rules on the assignment) such as "may only assign these roles", which is the safer way to let a pipeline grant access.
What you can now do
- Read a role definition and tell control-plane actions from dataActions.
- Work out effective access from assignments at and above a scope.
- List assignments without falling into the subscription-only default.