Infrastructure

Azure RBAC: diagnose authorization drift before widening a role

A production runbook for qualifying Azure RBAC drift with the real identity, scope, PIM, role assignments, deny assignments, logs, validation and rollback before adding permissions.

27 Jun 2026 azurerbacidentityentra-idpimsecurityobservabilitykqlrunbookrollbackautomation

An Azure RBAC denial usually appears at the worst moment: a pipeline can no longer deploy, a managed identity cannot read a resource, an operator loses access to a resource group, or automation fails on an action that used to be approved. The dangerous reflex is to add a broader role quickly. In production, that can hide scope drift, a missing PIM activation, a deleted role assignment, a deny assignment, a different execution identity or a propagation delay.

The use case is an Azure platform operated by product, platform and security teams. Deployments use federated or managed identities, some permissions are activated through PIM, and RBAC changes are managed through IaC or access approval workflows. An incident appears: a legitimate action returns AuthorizationFailed, but the team does not yet know whether the issue is role, scope, identity, timing or a security control. The runbook goal is to diagnose before widening permissions.

Fix the denied action

Before discussing roles, isolate the exact action. Azure RBAC authorizes a combination of principal, action, resource, scope and sometimes condition. If one of those elements is unclear, the correction will probably be too broad.

text rbac-denial-scope.txt
Incident
Symptom: AuthorizationFailed during deployment
Operation: Microsoft.Web/sites/config/write
Resource: /subscriptions/.../resourceGroups/rg-prod/providers/Microsoft.Web/sites/app-orders-prod
Expected principal: mi-platform-deploy-prod
Observed principal: confirm in logs
Expected scope: resource group rg-prod
Window: 2026-06-27 09:00-09:30 UTC

Questions before correction
Which exact action was denied?
Which principal really called Azure?
Is the denial on the target resource or on a dependency?
Is the missing role permanent, PIM eligible or managed by IaC?
Is a deny assignment or security policy blocking the action?

This step prevents a subscription-level correction for a denial that only concerns a child resource. It also avoids assigning rights to the wrong identity.

Prove the real identity

The principal documented in the runbook is not always the principal calling Azure. A pipeline can use a different OIDC federation, a job can fall back to a system-assigned identity, a local script can use the operator account, and automation can keep an old service principal.

bash 01-prove-runtime-identity.sh
az account show --query "{subscription:id, tenant:tenantId, user:user.name, type:user.type}" --output json

az ad signed-in-user show --query "{id:id,userPrincipalName:userPrincipalName}" --output json

az role assignment list --assignee "$PRINCIPAL_ID" --include-inherited --all --query "[].{role:roleDefinitionName,scope:scope,principalType:principalType}" --output table

For a managed or federated identity, also recover the objectId actually used in authentication logs, not only the display name. Names can be reused, moved or confused across environments.

Read the denial in logs

The tool-side error message is rarely enough. Activity Log gives the operation, status, caller and effective scope. Sign-in logs or pipeline traces connect the denial to the execution identity.

kusto 02-rbac-authorization-failures.kql
let window = 6h;
AzureActivity
| where TimeGenerated > ago(window)
| where ActivityStatusValue =~ "Failure"
| where OperationNameValue has_any ("WRITE", "DELETE", "ACTION")
| where Properties has "AuthorizationFailed" or StatusValue has "Forbidden"
| project TimeGenerated,
        OperationNameValue,
        ActivityStatusValue,
        Caller,
        ResourceGroup,
        ResourceProviderValue,
        ResourceId,
        CorrelationId,
        Properties
| order by TimeGenerated desc

If the environment centralizes Entra ID logs, add sign-in failures, OIDC claims and PIM events. The diagnosis must answer a simple question: was the identity authenticated correctly and then denied by RBAC, or did the failure happen earlier in the chain?

Compare expected and effective assignments

A missing role is only a hypothesis. Compare expected state with effective state, including inheritance, groups, temporary assignments, conditions and recent deletions.

bash 03-capture-role-assignments.sh
SCOPE="/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-prod"
PRINCIPAL_ID="11111111-1111-1111-1111-111111111111"

az role assignment list --scope "$SCOPE" --assignee "$PRINCIPAL_ID" --include-inherited --all --query "[].{role:roleDefinitionName,scope:scope,condition:condition,createdOn:createdOn,updatedOn:updatedOn}" --output json

az role assignment list --scope "$SCOPE" --include-groups --all --query "[?principalId=='$PRINCIPAL_ID' || contains(principalName, 'platform')].{principal:principalName,role:roleDefinitionName,scope:scope}" --output table

Archive this capture before correction. If rollback is needed, the team must know which assignment was added, modified or removed.

Check PIM, deny assignments and conditions

RBAC incidents are not limited to roleAssignments. An expired PIM activation, an eligible role that was never activated, a deny assignment created by a managed service, or an ABAC condition can produce the same operational symptom.

text rbac-hidden-controls-checklist.txt
Controls to check
PIM: eligible role but not active
PIM: activation expired or different scope
Group: membership not propagated or wrong group
Deny assignment: protected resource through managed service or historical blueprint
Condition: role assignment limited by attribute or resource type
Management group: role inherited above the observed scope
IaC: manual fix that will disappear at the next apply
Propagation: change too recent to conclude

A broader role does not always solve those controls. It can also create a lasting exception that will disappear at the next IaC deployment or bypass the expected access model.

Qualify the minimum need

The right correction starts from the denied action, not from the most comfortable role. Build a matrix that maps the operation to the smallest acceptable role, then verify whether the scope should be resource, resource group or subscription.

text rbac-minimal-access-matrix.txt
Action to authorize
Operation: Microsoft.Web/sites/config/write
Target resource: app-orders-prod
Justification: application configuration update by pipeline
Candidate role: Website Contributor or existing custom role
Preferred scope: resource or resource group rg-prod
Duration: permanent through IaC or temporary through PIM depending on operating model

Reject as quick correction
Contributor on subscription
Owner to bypass a write denial
Direct assignment to a user when the runbook expects a pipeline identity
Manual role not declared in IaC
Addition without proof of the calling identity

If the minimum role does not exist, the decision may be to create or fix a custom role. But that choice must remain explicit. It is not equivalent to opening Contributor for ten minutes.

Decide: wait, correct or roll back

The diagnosis should produce an operable decision. Some cases only require waiting for propagation or activating PIM. Others require a limited RBAC correction. Others require rolling back a recent access change.

text rbac-decision-matrix.txt
Wait or replay
Role assignment is correct and very recent
RBAC propagation is plausible
Scope and identity did not change
Controlled replay is available without user impact

Correct at minimal scope
Real identity confirmed
Denied action understood
Missing role identified
Target scope bounded
Change declared in IaC or access ticket

Activate PIM
Eligible role is already approved
Operating window is limited
Justification and approver exist
Activation trace is retained

Roll back
Recent RBAC deletion is not justified
Group or role change broke several consumers
Deny assignment or policy was introduced by mistake
Quick correction would broaden access too much

The practical rule is simple: do not widen a role until the real identity, denied action and minimum scope are proven.

Validate after correction

An RBAC correction is not validated only because a deployment passes again. The team must prove that the expected action works and that permissions did not spill over.

text rbac-validation-and-rollback.txt
Minimum validation
The same principal performs the same action without AuthorizationFailed
The assignment scope matches the need
No broad role was added as a shortcut
Logs show the successful call and expected caller
Non-regression tests pass for other consumers
The change is captured in IaC, PIM or an approved access ticket

Clean rollback
Remove only the assignment that was added
Restore the deleted assignment if the cause was removal
Replay the validation action
Keep correlationId, caller, role and scope in the ticket
Add the case to the access runbook if the incident was reproducible

If the correction was done manually to unblock the incident, immediately plan its reconciliation into the normal mechanism: Terraform, Bicep, IAM workflow or PIM. Otherwise, the drift will return.

Automate guardrails

Good RBAC automation should not only create assignments. It should prevent overbroad emergency corrections and retain the evidence needed to diagnose the next denial.

yaml rbac-change-guardrails.yml
guardrails:
before_apply:
  - capture_denied_operation
  - prove_runtime_principal
  - list_effective_assignments
  - check_pim_and_deny_assignments
  - map_action_to_minimal_role
block_when:
  - contributor_or_owner_at_subscription_without_exception
  - runtime_identity_unknown
  - manual_assignment_not_reconciled_with_iac
  - missing_rollback_plan
after_apply:
  - replay_denied_action
  - query_activity_log_for_success
  - confirm_no_broader_scope_added
  - attach_evidence_to_access_ticket

The goal is not to slow every access request. The goal is to avoid turning an authorization incident into permanent security debt.

Conclusion

An Azure RBAC denial should be treated as an access-control incident, not as an invitation to add a broader role. The healthy sequence is to isolate the denied action, prove the real identity, compare effective assignments, check PIM, deny assignments and conditions, then decide.

The right outcome is straightforward: wait when propagation explains the symptom, activate PIM when the right already exists, correct at minimal scope when the gap is proven, or roll back when recent drift broke access. That unblocks production without turning urgency into excessive long-lived permission.