Automation
Azure Policy: diagnose an expired exemption before disabling the assignment
A production runbook for tying expiry, scope, assignment and compliance together before renewing an Azure Policy exemption or disabling the guardrail.
An Azure pipeline deployed successfully yesterday. Today, the same artifact fails with RequestDisallowedByPolicy. The assignment was not changed and the exemption is still visible in Azure. Under pressure, two shortcuts look attractive: extend its date without further checks, or switch the assignment to DoNotEnforce to unblock production.
The trap is in the exemption lifecycle. Once an exemption reaches expiresOn, Azure no longer honors it, but keeps the object for the record. Seeing it is therefore not proof that it still covers the resource. This runbook drives a narrower decision: fix the resource, renew the exemption temporarily, repair its scope or references, or preserve the denial without weakening the entire guardrail.
Freeze the first denial and the expected contract
Start with the first rejected deployment, not the latest retry. Preserve the commit, artifact, UTC time, resource, ARM operation, correlation ID and complete error. Then reconstruct the exemption contract that was originally approved.
incident:
pipeline_run: deploy-prod-20261004.3
commit: 8d2c1f4
first_deny_utc: 2026-10-04T05:47:12Z
target_resource: /subscriptions/<sub>/resourceGroups/rg-prod-app/providers/Microsoft.Web/sites/orders-api
operation: Microsoft.Web/sites/write
error: RequestDisallowedByPolicy
correlation_id: <correlation-id>
expected_exemption:
name: orders-api-diagnostics-waiver
assignment: deny-missing-diagnostic-settings
category: Waiver
owner: platform-operations
approved_ticket: CHG-1842
expected_expiry_utc: 2026-10-04T05:30:00Z
exit_condition: deploy diagnostic settings from the application template If nobody can name the accepted risk and exit condition, renewal is not a technical restoration. It is a new risk decision that needs an owner.
Prove the effective exemption state
Read the object at its exact scope. Capture expiresOn, category, assignment, definition references, selectors and metadata. Compare the UTC date with the first denial.
SCOPE="/subscriptions/<sub>/resourceGroups/rg-prod-app/providers/Microsoft.Web/sites/orders-api"
EXEMPTION="orders-api-diagnostics-waiver"
az policy exemption show --name "$EXEMPTION" --scope "$SCOPE" --query '{id:id,name:name,assignment:policyAssignmentId,category:exemptionCategory,expiresOn:expiresOn,references:policyDefinitionReferenceIds,selectors:resourceSelectors,metadata:metadata,systemData:systemData}' --output jsonc
date -u +'%Y-%m-%dT%H:%M:%SZ' A past date explains why the exemption is no longer applied. It does not yet prove that today’s deny represents the risk previously accepted. If expiresOn is absent or still in the future, investigate the wrong scope, a different assignment, a changed initiative reference or an uncovered child resource.
Do not immediately recreate the exemption under a new name. That breaks the direct link to its approval, history and incident-defining expiry.
Map the denial to the exact assignment
The ARM error should expose the assignment ID and, for an initiative, the policyDefinitionReferenceId. Compare them with the retained exemption. Matching display names are not evidence; full resource IDs must line up.
ASSIGNMENT_ID="/providers/Microsoft.Management/managementGroups/mg-prod/providers/Microsoft.Authorization/policyAssignments/landing-zone-guardrails"
az policy assignment show --ids "$ASSIGNMENT_ID" --query '{id:id,scope:scope,enforcementMode:enforcementMode,definition:policyDefinitionId,parameters:parameters,notScopes:notScopes}' --output jsonc
DEFINITION_ID=$(az policy assignment show --ids "$ASSIGNMENT_ID" --query policyDefinitionId -o tsv)
az policy set-definition show --id "$DEFINITION_ID" --query 'policyDefinitions[].{reference:policyDefinitionReferenceId,definition:policyDefinitionId,parameters:parameters}' --output table If an initiative replaced a reference, the old exemption can remain readable without covering the new rule. If the assignment was recreated, its ID may have changed. If the exemption lives on a resource group, confirm that the denied resource is beneath that hierarchy. Fix the proven mismatch; do not widen scope just to remove the error.
Confirm that non-compliance is still the same
An expired exemption can coincide with genuine drift. Export the current resource state and compare it with the Policy condition. In this case, the exemption temporarily accepted missing diagnostic settings while a template was being fixed. If that fix never shipped, expiry is working as designed. If diagnostics exist, the denial may come from an alias, parameter or changed initiative.
RESOURCE_ID="$SCOPE"
az policy state list --resource "$RESOURCE_ID" --query "[?policyAssignmentId=='$ASSIGNMENT_ID'].{timestamp:timestamp,state:complianceState,assignment:policyAssignmentName,definition:policyDefinitionName,reference:policyDefinitionReferenceId,effect:policyDefinitionAction}" --output table
az resource show --ids "$RESOURCE_ID" --query '{id:id,type:type,location:location,tags:tags,properties:properties}' --output json > resource-effective-state.json Compliance state may trail the transactional denial. Use it to confirm context without waiting indefinitely for a view to converge. The deployment error, effective assignment and actual resource state are the primary evidence.
Rebuild the timeline without confusing expiry and deletion
Expiry does not delete the object. Search separately for exemption writes, assignment changes and rejected deployments. A short timeline distinguishes a normal deadline from an unannounced governance change.
let StartTime = datetime(2026-10-03T18:00:00Z);
let EndTime = datetime(2026-10-04T07:00:00Z);
AzureActivity
| where TimeGenerated between (StartTime .. EndTime)
| where ResourceProviderValue =~ "MICROSOFT.AUTHORIZATION"
or Properties has "RequestDisallowedByPolicy"
| where ResourceId has "policyExemptions"
or ResourceId has "policyAssignments"
or Properties has "policyAssignmentId"
| project TimeGenerated,
OperationNameValue,
ActivityStatusValue,
Caller,
ResourceId,
CorrelationId,
Properties
| order by TimeGenerated asc The absence of a delete operation is expected for simple expiry. It justifies neither automatic renewal nor disabling the assignment.
Decide between correction and bounded renewal
Fix the resource when the original exit condition is achievable: missing diagnostic settings, absent tags, incomplete encryption or a non-compliant SKU. Fix the exemption when approval remains valid but its mapping is wrong: stale assignment ID, obsolete initiative reference or scope narrower than the approved resource.
Renewal is defensible only when the risk is still accepted, the scope has not grown, an owner confirms a new deadline and the permanent fix has a scheduled change. Update the existing object to preserve continuity.
NEW_EXPIRY="2026-10-04T18:00:00Z"
az policy exemption update --name "$EXEMPTION" --scope "$SCOPE" --expires-on "$NEW_EXPIRY" --metadata '{"owner":"platform-operations","ticket":"CHG-1907","exit":"deploy diagnostic settings"}' --output jsonc Do not change enforcementMode, the definition effect or notScopes to treat one workload. Those changes move the decision to every resource under the assignment and make rollback substantially riskier.
Canary the same artifact and prepare rollback
After correction or renewal, first replay one bounded operation with the same commit and pipeline identity. Verify that the expected denial disappears only for the approved resource and that a control resource outside the exemption is still denied.
Allowed canary
Same artifact and parameters as the rejected run
Same resource covered by the exemption
No changes outside the approved plan
Healthy application probe after deployment
Denial control
Control resource outside scope
Same non-compliant condition
Deny still emitted by the expected assignment
Rollback
Stop retries if the denial changes rule or scope
Restore the previous artifact if the application fix regresses
Restore the old expiresOn or delete a replacement according to audit policy
Never leave DoNotEnforce as a temporary workaround
Preserve correlation IDs, diff and final decision Close the incident only after compliance is reread following propagation and the exemption exit has a date. A green pipeline without a denial control proves only that a barrier disappeared.
Conclusion
A visible Azure Policy exemption can be fully expired: the object remains as evidence, but Azure no longer honors it. Diagnosis must tie the denial time to expiresOn, the exact assignment, initiative references and the actual resource state.
The production decision then becomes clear: fix non-compliance, repair one mapping, renew briefly under fresh approval, or keep the denial. Final validation must prove both the allowed deployment and the refusal that still protects the rest of the scope.