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.

04 Oct 2026 azureazure-policypolicyexemptiongovernanceiaccomplianceobservabilityautomationrunbookvalidationrollbackproduction

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.

yaml policy-exemption-incident.yml
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.

bash 01-read-effective-exemption.sh
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.

bash 02-compare-assignment-and-initiative.sh
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.

bash 03-read-policy-state.sh
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.

kusto 04-policy-exemption-timeline.kql
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.

bash 05-renew-bounded-exemption.sh
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.

text policy-exemption-validation.txt
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.