Tight least‑privilege changes commonly break Azure apps when permissions do not match real operations. Restore availability by mapping operations to roles, running staging smoke tests, and applying scoped temporary fixes. Expect recovery within 1 to 3 hours for common storage breaks.
Process summary
This summary lists steps to detect, contain, and fix permission outages. Each step must run as a gated task with CI tests before production changes.
- Inventory resources and map operations to permissions. Use Azure Resource Graph and app telemetry.
- Reproduce the failure in staging and capture Activity Log and Entra sign‑in traces.
- Apply a narrow temporary role scoped to the resource and run a smoke test.
- Codify the permanent fix as a custom role or scoped built‑in role and add CI smoke tests.
- Enforce change control with Azure Policy and PR gating.
Step 1: inventory and permission mapping
Collect an inventory of resources and identities before changing permissions. The inventory must show resources, identities, and all operations each app performs.
Discover identities and their scopes
Query all service principals, managed identities, and app registrations in the tenant. Export role assignments and scope for each principal.
- Run az ad sp list and az identity list to list principals and managed identities.
- Run az role assignment list --assignee to snapshot current assignments.
The most frequent error at this point is removing broad roles without capturing operation calls. Capture roleAssignment/write events first.
Map operations to API calls
Capture application telemetry that shows which APIs the code calls. Correlate HTTP routes, SDK calls, and background job traces to provider operations.
- For Microsoft Graph calls, note exact REST endpoints used by the app.
- For storage operations, record container, blob, queue, and table API calls.
Map each background job and web route to a concrete provider operation, then to the smallest built‑in role that contains that operation.
Create the permission→operation matrix
Build a table that lists operations, minimal built‑in role, and example scope. Start with Azure AD, Key Vault, Storage, SQL, Service Bus, and AKS.
- Map Azure AD operations to Graph API app or delegated permissions.
- Map Key Vault secret Get and List to Key Vault data roles or access policies.
- Map Storage blob read and write to Storage Blob Data roles or SAS tokens.
A machine‑readable permission→functionality matrix closes the gap between auditing and actionable change. Store CSV or JSON next to CI artifacts so tests can read it and confirm permissions.
Storing the matrix as JSON enables programmatic mapping, automated diffs on access reviews, and reproducible evidence for auditors. This turns the matrix into continuous, testable telemetry.
Step 2: diagnostics and reproduce failures
Reproducing the failure in staging is essential before changing production roles. A repeatable reproduction limits blast radius and supplies evidence for auditors.
Capture the exact telemetry to collect
Collect these logs at the incident timestamp and store them with the ticket ID. Include Activity Log, resource diagnostics, and Entra sign‑in traces.
- Azure Activity Log: filter by operationName and caller. Look for roleAssignment/write and roleAssignment/delete.
- Resource diagnostic logs: Key Vault audit events, Storage analytics 403 entries, SQL error logs.
- Entra sign‑in logs: AADSTS errors and correlation IDs.
A common case: scheduled ETL tasks fail overnight and show only a single 403 in storage analytics. Synthetic jobs prevent missing such failures.
Reproduce with synthetic tests
Create a minimal test that executes the failing operation. Run it under the same identity that failed in production.
- For Storage: az storage blob upload to the target container.
- For Key Vault: curl to the vault endpoint using a Managed Identity token.
If the synthetic test returns 403, the cause is a missing RBAC permission or an incorrect SAS scope. If it returns AADSTS errors, the cause is a credential or app registration mismatch.
Read common error codes and what they indicate
- 403 from resource APIs usually means RBAC or SAS scope issues.
- AADSTS7000215 means invalid client secret or expired certificate.
- MSI endpoint 404 or 400 means no identity assigned or IMDS issues.
The data points to a narrow cause: missing role assignments or expired credentials. Use the correlation ID to trace the sign‑in event.
This method is not appropriate for purely managed SaaS where the vendor controls access, or for end‑of‑life apps scheduled for decommissioning. During active incidents prioritize availability over refactoring.
When an outage occurs, precise Log Analytics queries and az CLI commands speed root cause analysis. Use AzureActivity and SigninLogs to correlate roleAssignment events and sign‑ins.
Use the extracted correlation ID to pivot between SigninLogs and resource diagnostics. When AADSTS codes appear, include the code in the search to find expired certificates or missing app permissions.
These queries and commands give reproducible steps to confirm whether a 403 is RBAC, SAS scope, or credential related.
1. Detect
Synthetic test fails or alert fires
2. Diagnose
Collect Activity Log, Entra sign‑ins, resource diagnostics
3. Contain
Apply temporary scoped role and re‑run smoke test
4. Fix
Codify custom role or scoped built‑in role and add CI test
Step 3: reproducible snippets and CI tests
Provide failing deployments and the minimal fixes as code. Each snippet reproduces a broken least‑privilege state and shows the corrected role assignment.
Failing scenario: remove broad role
Run these commands to simulate a break where a managed identity loses broad rights and storage operations fail. The first command removes the broad role. The second command triggers the storage operation.
Bash
az role assignment delete --assignee "" --role "Contributor" --scope "/subscriptions//resourceGroups/"
az storage blob upload --account-name "" --container-name "" --name test.txt --file ./test.txt
The expected 403 appears in storage analytics and the Activity Log. That provides evidence for the role gap.
Minimal fix: scoped blob role
Apply only the storage data role on the container scope. This avoids granting write rights elsewhere.
Bash
az role assignment create /
--assignee "" /
--role "Storage Blob Data Contributor" /
--scope "/subscriptions//resourceGroups//providers/Microsoft.Storage/storageAccounts//blobServices/default/containers/"
Bicep custom role snippet
Replace the JSON example with a Bicep role definition and assignment so readers can deploy it natively. The snippet creates a custom role and assigns it at container scope.
Bicep
param storageAccountName string = ''
param containerName string = ''
param principalId string
resource storage 'Microsoft.Storage/storageAccounts@2022-09-01' existing = {
name: storageAccountName
}
resource container 'Microsoft.Storage/storageAccounts/blobServices/containers@2021-09-01' existing = {
parent: storage
name: 'default/${containerName}'
}
resource customRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' = {
name: guid(subscription().id, storageAccountName, containerName, 'customBlobWriter')
properties: {
roleName: 'Custom Storage Blob Writer'
description: 'Writes and reads blobs in a specific container'
type: 'CustomRole'
permissions: [
{
actions: [
'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read',
'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/write'
]
notActions: []
}
]
assignableScopes: [storage.id]
}
}
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
name: guid(container.id, principalId, 'assignment')
properties: {
roleDefinitionId: customRole.id
principalId: principalId
principalType: 'ServicePrincipal'
}
scope: container
}
This Bicep sample creates a custom role and assigns it at the container scope so readers can deploy a precise, scoped fix.
CI test: GitHub actions smoke job
This pipeline runs a minimal operation as the staging principal and fails on 403; add it to PR gates for identity or role assignment changes.
Yaml
name: permission-smoke
on: [pull_request]
jobs:
smoke:
runs-on: ubuntu-latest
steps:
- uses: azure/login@v1
with:
client-id: ${{ secrets.STAGING_CLIENT_ID }}
tenant-id: ${{ secrets.AZ_TENANT }}
client-secret: ${{ secrets.STAGING_CLIENT_SECRET }}
- name: Run blob upload smoke
run: |
set -e
az storage blob upload --account-name "$STORAGE_ACCOUNT" --container-name "$CONTAINER" --name smoke.txt --file ./smoke.txt || (echo "permission test failed"; exit 1)
Include such jobs in PR gates to prevent permission regressions from reaching production.
Provide reproducible IaC examples that show both the broken and fixed states so teams can run sandboxed drills. Include Terraform, Bicep, and az CLI variations.
Pause briefly.
Step 4: runbooks for rollback and controlled exceptions
Operational runbooks reduce MTTR and give compliance evidence. Each runbook must be short and repeatable.
On‑call rollback play
- Identify the failing principal and affected resources.
- Apply the narrow temporary assignment with an expiry tag and ticket ID.
- Run the synthetic test and verify success.
Record roleAssignment/write events and attach logs to the incident ticket. Automate removal when the TTL expires.
Controlled exception process
Define a break‑glass approval that requires two approvers and a 24 hour maximum TTL for service principals. Log every exception in the change control system.
Use Just‑in‑Time elevation via PIM for human roles and avoid PIM for unattended services. Many organizations assume PIM covers all needs, but often omit continuous testing of background jobs.
Monitoring and alerts
Alert on anomalous roleAssignment deletions and failed synthetic tests. Set an SLO for synthetic tests, for example five consecutive failures before paging.
Store artifacts and logs for post‑mortem and compliance reviews. Configure Azure Policy to require diagnostic settings for critical resources.
PIM, RBAC, and managed identities compared
Choosing the correct control reduces outages and audit effort. The table compares common choices by use case and risk.
| Use case |
Identity type |
Minimal role |
PIM/JIT applicable |
Risk level |
| Background ETL job |
Managed identity |
Storage Blob Data Contributor (scoped) |
No |
Medium |
| Admin console access |
User + PIM |
Privileged role via PIM |
Yes |
High |
| Service to service API calls |
Service principal or managed identity |
App permission or scoped data role |
No for service accounts |
Medium |
Errors that ruin the result
This section lists common mistakes that lead to outages and clear guardrails to avoid them. Each entry shows what to do instead of risky moves.
Removing Contributor/Owner lightly
Removing subscription or resource group Contributor from service identities often breaks rarely used code paths. Those paths include admin APIs and recovery jobs.
- Always map API calls before removing broad roles.
- Use Activity Log to find calls that used the deleted role.
Trusting managed identities without checks
Managed identities reduce secret handling, but long‑running jobs and third‑party integrations can need different scopes or app permissions. A common case is a nightly job that used delegated Graph calls; switching to a managed identity dropped delegated scopes and the job failed.
Changing permissions in production
Applying role changes directly in production raises risk. A PR with a CI smoke test prevents most outages.
- Add permission smoke tests to pull request gates for role changes.
- Use a staging environment that mirrors production scopes.
Pause briefly to separate operating guidance from examples.
Decision checklist: when to tighten or relax privileges
This checklist helps decide whether to apply least privilege now or delay the change for a safer window.
- Does the change affect unattended service principals or managed identities? If yes, run CI smoke tests.
- Are there synthetic jobs that cover the affected operations? If no, create them first.
- Is there a scheduled maintenance window with rollback authority? If no, delay high‑risk changes.
The evidence shows changes validated by CI smoke tests reduce permission incidents by measurable amounts. Plan a two‑week pilot that uses the CI tests, role definitions, and runbooks during a controlled maintenance window.
Simulated case studies and measurable outcomes
Two reproducible case studies compare a broken least‑privilege change and the corrected approach. Both are scripted so teams can run them in a sandbox.
Case study 1: nightly ETL stopped by role removal
Situation: a managed identity lost Contributor access on the resource group. The nightly ETL job then failed during container writes.
Actions taken: the team reproduced the failure in staging, applied a scoped Storage Blob Data Contributor role to the container, and re‑ran the ETL job. The job succeeded after the scoped assignment.
Outcome: restoring the scoped role returned the job within 30 minutes. The team then codified the fix as a custom role and added a CI smoke test.
Case study 2: key vault read failures after role tightening
Situation: an app lost Key Vault data Get permissions after a role review. The app returned 403 on secret reads.
Actions taken: the team captured Key Vault audit events, matched the operation to the minimal Key Vault data role, and applied a scoped assignment for the vault. The app resumed in under 45 minutes.
Outcome: the permanent fix used a custom role and scheduled access reviews to avoid regressions.
Actionable synthesis and recommended next steps
Map operations to roles first, then build CI smoke tests that run as part of PR gating. Use staged, scoped temporary fixes for containment during incidents. Codify permanent fixes as custom roles and schedule access reviews.
Apply the permission→operation matrix to at least three critical apps in the first two weeks. That pilot yields measurable reductions in permission incidents and faster MTTR.
If assistance is needed for a targeted audit or runbook, contact the security operations team through normal change channels.
FAQ
What is the fastest way to restore a storage write operation that just broke?
Apply a scoped Storage Blob Data Contributor role to the affected container and run a smoke upload. Then capture Activity Log entries and storage analytics 403 rows.
How to prove a 403 was caused by RBAC and not by SAS scope?
Reproduce the failing operation with the same identity in staging. If az storage blob upload fails with 403 under that identity, RBAC or SAS scope is the likely cause.
How to handle expired certificates that show as AADSTS errors?
Search SigninLogs for the AADSTS code and correlation ID. Rotate the expired certificate, update the app registration, and revalidate sign‑ins.
Can PIM handle unattended service principals and reduce outages?
PIM works for human roles but not for unattended services. Use scoped roles and CI tests for service accounts instead.
How to prevent permission regressions in CI/CD pipelines?
Add a permission smoke job that runs the minimal API calls as the target principal. Fail the PR on 403 responses.
When should a team delay least‑privilege changes until a maintenance window?
Delay changes when no synthetic coverage exists or when no rollback authority is scheduled. Make changes during a window with clear rollback steps and test coverage.
Closing recommendations and supporting artifacts
Keep the permission→operation matrix as JSON beside CI artifacts so tests can read it. Run permission‑induced outage drills for Storage, Key Vault, and SQL during the pilot. Ensure every temporary assignment has TTL and ticket linkage for audit evidence.