Six months after you roll out a deny policy that blocks public storage accounts, a senior engineer pings you: “Your governance broke our deploy. The legacy export job needs a public account for a partner who can’t do private endpoints yet. Security signed off, give us a pass.” You have three ways to give that pass, and two are wrong. You could weaken the policy (now everyone can go public). You could add the resource group to the assignment’s notScopes — an exclusion — and the account vanishes from compliance entirely, as if the rule never existed, with no record of why and no expiry. Or you can grant a policy exemption: the resource stays in scope, the policy still knows about it, it shows in the dashboard as Exempt rather than non-compliant, it carries a category that says whether you accepted the risk or fixed it another way, and it expires on a date you set. Only the third option is governance. The other two are how a clean compliance posture quietly rots into a lie.
This article is the mental model for getting that third option right. Azure Policy exemptions are the supported, auditable way to say “this specific scope is allowed to skip this specific assignment, for this reason, until this date” — without touching the policy, without weakening it for anyone else, and without making the resource disappear. The whole feature turns on a handful of distinctions that people conflate and then regret: exemption versus exclusion (waived-but-tracked versus never-evaluated), Waiver versus Mitigated (the two and only two exemption categories, and they mean genuinely different things to an auditor), scope (an exemption lives at a management group, subscription, resource group, or single resource and only ever narrows, never widens), and expiry (the one field that separates a disciplined carve-out from technical debt that outlives the people who created it).
By the end you will reach for an exemption only when it’s the right tool, set its category honestly, scope it as tightly as the exception demands, give it an expiry every time, target only the controls inside an initiative that need waiving, and read the dashboard knowing exactly what “Exempt” means. You’ll also know the traps cold — the exemption that silently outlived its expiresOn, the one that waived forty controls because someone exempted the initiative instead of one policyDefinitionReferenceId, the DeployIfNotExists remediation an exemption quietly stopped from running, and the RBAC gap that let a team self-exempt their own non-compliance.
What problem this solves
Policy at scale runs into reality. You assign a security baseline — deny public IPs on NICs, require HTTPS-only on web apps, enforce a specific set of allowed regions, audit untagged resources — across a management group, and within a week you have a fistful of legitimate exceptions: a break-glass jump box that genuinely needs a public IP, a vendor appliance that can’t do the tag schema, a data-residency-exempt workload that must run in a region your policy bans, a deprecated app scheduled for decommission next quarter that will never be brought into compliance. Every one of these is real. The question is what you do with them, and the wrong answers all share one trait: they erase the exception from the record.
What breaks without exemptions: people get the carve-out by damaging the control. They edit allowedValues to slip one region through (now the whole org can use it). They drop a resource group into notScopes so the assignment never sees it (the resource shows compliant-by-omission, indistinguishable from one that genuinely passes, forever). They set the effect to Disabled “just while we sort this out” and never re-enable it. Each makes the dashboard look green while the posture degrades, and none records who approved the exception, why, or when it ends. Six months later an auditor asks “why is this account public?” and the honest answer is “nobody remembers, and our tooling actively hid it.”
Who hits this: every team past the toy stage of governance. The moment you move from auditing to enforcing (Deny, Modify, DeployIfNotExists), exceptions become unavoidable — a hard deny with no exemption path gets ripped out the first time it blocks a legitimate Friday deploy. It bites hardest in landing-zone environments where a platform team sets policy at the management-group level and dozens of application teams need scoped, temporary relief without filing a ticket to weaken a shared control. The fix is not “make the policy softer” — it’s “grant a narrow, categorised, expiring exemption, and let the dashboard keep telling the truth.”
To frame the whole field before the deep dive, here is the menu of ways people grant an exception, and what each one actually does to your compliance posture:
| Mechanism | What it does | Resource shows as | Recorded reason? | Expires? | Verdict |
|---|---|---|---|---|---|
| Policy exemption | Suppresses this assignment for this scope, tracked | Exempt | Yes (category + metadata) | Yes (expiresOn) |
The right tool |
Exclusion (notScopes) |
Removes the scope from the assignment entirely | (not evaluated — “compliant” by omission) | No | No | Use only for permanent structural carve-outs |
Edit the policy / allowedValues |
Changes the rule for everyone under it | Compliant (rule now permits it) | No | No | Wrong — weakens the control globally |
Set effect to Disabled |
Turns the policy off | Not started / not evaluated | No | No | Wrong — blinds the whole assignment |
| Lower the assignment to Audit | Stops enforcing, keeps reporting | Non-compliant (but not blocked) | No | No | Wrong as an exception tool (it’s a posture choice) |
Learning objectives
By the end of this article you can:
- Explain precisely how an exemption differs from an exclusion (
notScopes) and from weakening a policy, and pick the right one for a given exception. - Choose between the Waiver and Mitigated exemption categories correctly, and defend the choice to an auditor.
- Place an exemption at the correct scope (management group, subscription, resource group, or single resource) and explain why an exemption can only narrow, never widen, an assignment.
- Set an expiry (
expiresOn) on every exemption and understand exactly what happens to evaluation and compliance the instant it passes. - Exempt only the specific controls inside an initiative (
policyDefinitionReferenceIds) that need waiving, rather than the whole initiative. - Create, list, and remove exemptions with
az policy exemptionand author them as Bicep, including the RBAC needed to do so safely. - Read the Exempt compliance state in the dashboard and understand how exemptions interact with DeployIfNotExists remediation, Deny enforcement, and Defender for Cloud secure score.
- Avoid the classic failure modes: the expired-but-still-trusted exemption, the over-broad initiative exemption, the self-service exemption that lets teams hide their own non-compliance, and the exemption that silently stopped a remediation.
Prerequisites & where this fits
You should already understand Azure Policy at the level of: a policy definition is a rule with an effect (Audit, Deny, Modify, DeployIfNotExists, and so on); an initiative (policy set) bundles many definitions under one assignment; and an assignment binds a definition or initiative to a scope (a management group, subscription, or resource group), at which point everything under that scope is evaluated. If those terms are fuzzy, read Azure Policy Effects Decoded: Deny vs Audit vs Modify vs DeployIfNotExists and Policy Definitions vs Initiatives: When to Bundle Controls into a Set first — exemptions only make sense once assignments do.
You should be comfortable running az in Cloud Shell, reading JSON, and you need write permission on policy exemptions at the scope in question (covered in Security notes). Knowing the Management Groups hierarchy helps, because exemption scope and assignment scope both live on that tree and their relationship is the crux of the whole feature.
This sits in the Governance track, downstream of policy authoring and assignment. It is the operational half of policy: assignment is how you set the rules, exemptions are how you run the rules in the real world where exceptions exist. It pairs tightly with compliance reporting — an exemption’s entire reason for existing is to keep the compliance dashboard honest — and with Azure RBAC Fundamentals, because who may grant an exemption is itself a governance control. Here is the quick map of where each piece lives and who owns it:
| Layer | What lives here | Who usually owns it | Relevance to exemptions |
|---|---|---|---|
| Management group | Policy + initiative assignments, org-wide | Platform / cloud governance | Exemptions here waive broadly — handle with care |
| Subscription | Workload-level assignments, exemptions | Platform + workload lead | Common scope for “this whole sub is exempt from X” |
| Resource group | App-level resources, scoped exemptions | Application team | The usual scope for a real, narrow exception |
| Single resource | One offending resource | Application team | Tightest, safest exemption scope |
| RBAC (write on exemptions) | Who may grant a carve-out | Governance team | The gate that stops self-service non-compliance |
Core concepts
Five mental models make every later decision obvious.
An exemption is a tracked waiver, not a deletion. When you exempt a scope from an assignment, the resource is still in scope — the policy still knows it exists — but evaluation is suppressed and its compliance state becomes Exempt instead of compliant or non-compliant. This is the most important distinction in the whole feature. An exclusion (notScopes on the assignment) does the opposite: it removes the scope from the assignment so the resource is never evaluated at all, then reports as compliant-by-omission, indistinguishable from one that genuinely passes. Exemption keeps the exception visible and accountable; exclusion makes it invisible. You want visible.
The category is a statement to an auditor, and there are exactly two. Every exemption carries an exemptionCategory of precisely Waiver or Mitigated — no third value exists. Waiver means “we accept this non-compliance as a known risk for now” (the work isn’t done; we live with the gap, usually temporarily). Mitigated means “the control’s intent is met by other means, so the check here is a false positive” (a compensating control exists). Choosing right isn’t bureaucracy — a Waiver tells an auditor “open risk, track it”; a Mitigated tells them “satisfied elsewhere, here’s the compensating control.” Mislabel them and you either alarm auditors over handled risks or hide real ones.
Scope only narrows; an exemption can never grant more than its assignment. An exemption targets one policy assignment at a scope that must be at or below that assignment’s scope. If a deny policy is assigned at a management group, you can exempt a child subscription, a resource group inside it, or a single resource — but you cannot “un-assign” the policy above the assignment, and an exemption at one RG has no effect on a sibling RG. Exemptions flow down like assignments do: one at a subscription covers everything under it; one at a single resource covers only that resource. You are always subtracting a sub-tree from an assignment’s reach, never adding to it.
Expiry is a first-class field, and crossing it re-arms the policy automatically. An exemption can carry expiresOn — a UTC timestamp after which it stops applying. The moment it passes, the resource is evaluated against the assignment again on the next cycle: if still non-compliant it goes back to Non-compliant (and a Deny effect blocks new writes again). The object itself isn’t deleted — it lingers as an expired exemption to clean up — but its effect ends. expiresOn is optional in the schema, which is exactly the trap: an exemption with no expiry is permanent, and permanent exceptions are how governance rots. Treat the field as mandatory even though the platform doesn’t.
An exemption can waive a whole assignment or just specific controls within an initiative. When the assignment is an initiative (a set of, say, forty definitions), an exemption by default waives the entire initiative for the scope — almost never what you want. The policyDefinitionReferenceIds field lets you list the specific reference IDs of the definitions this exemption applies to, leaving the rest enforced. Exempting “require HTTPS-only” for one legacy app should not also exempt “encrypt at rest,” “enforce TLS 1.2,” and every other control in the baseline. Granular reference targeting is what keeps an exemption surgical.
The vocabulary in one table
Before the deep sections, pin down every moving part. The glossary repeats these for lookup; this is the mental model side by side:
| Concept | One-line definition | Where it lives | Why it matters |
|---|---|---|---|
| Policy assignment | A definition/initiative bound to a scope | On a scope (MG/sub/RG) | The thing an exemption targets |
| Exemption | A tracked, scoped, expiring waiver of an assignment | Microsoft.Authorization/policyExemptions |
The right tool for a real exception |
Exclusion (notScopes) |
A scope removed from the assignment entirely | Assignment property | Resource never evaluated — use sparingly |
exemptionCategory |
Waiver or Mitigated — why the exemption exists |
Exemption property | The statement to an auditor |
| Scope (of exemption) | MG / sub / RG / resource the waiver applies to | Exemption’s scope | Must be at/under the assignment scope |
expiresOn |
UTC time the exemption stops applying | Exemption property | Separates discipline from rot |
policyDefinitionReferenceIds |
Which definitions in an initiative to waive | Exemption property | Keeps initiative exemptions surgical |
| Exempt (compliance state) | The dashboard state of an exempted resource | Compliance results | Visible, accountable exception |
assignmentScopeValidation |
Whether the exemption scope must be in the assignment’s reach | Exemption property | Guards against mis-scoped waivers |
| Resource Policy Contributor | Built-in role that can write exemptions | RBAC role definition | The permission gate |
Exemption vs exclusion vs the wrong fixes
This is the fork everyone gets wrong, so spend a minute here before anything else. Four mechanisms can make a resource “stop failing a policy,” and they are profoundly different in what they do to your records. The table is the whole argument; the prose explains the rows that bite.
| Mechanism | How you do it | What the policy does | Compliance result | Audit trail | Blast radius |
|---|---|---|---|---|---|
| Exemption | Create a policyExemptions object at the scope |
Stops evaluating that scope for that assignment | Exempt | Category + metadata + (ideally) expiry | Exactly the scope you set |
| Exclusion | Add scope to the assignment’s notScopes |
Never evaluates that scope at all | Not evaluated (looks compliant) | None on the resource | The whole sub-tree you excluded |
| Weaken the policy | Change allowedValues / rule logic |
Permits the thing for everyone | Compliant (rule changed) | Policy version history only | Every scope the definition applies to |
| Disable the effect | Set effect to Disabled |
Turns the policy off | Not started | Assignment history only | The entire assignment |
Exemption vs exclusion is “tracked” vs “vanished.” An exclusion via notScopes is the right call for a permanent, structural carve-out where the resource has no business being evaluated — a dedicated sandbox subscription you’ve deliberately put outside a production-grade baseline. It’s the wrong call for a temporary or risk-bearing exception, because an excluded resource looks identical to a compliant one — nothing on it to review, no category, no expiry, no reason. If you ever want to report “here are the things we’re knowingly letting slide and why,” you need exemptions. Cleanly put: exclusion answers “this is not in the population we govern”; exemption answers “this is in the population, and here’s our documented exception for it.”
Weakening the policy or disabling the effect is never an exception tool. Both change the rule for the whole assignment, not one resource. Editing allowedValues to let one workload use southindia opens that region to every subscription under the assignment; setting the effect to Disabled blinds the policy entirely. People reach for these because they’re fast and make the red go away, but they trade one resource’s exception for an org-wide hole and leave no trace on the resource that needed it. Where you find these, they’re almost always an exemption done wrong.
The decision is mechanical once you frame it as two questions:
| If the exception is… | …and the resource should be… | Use |
|---|---|---|
| Temporary or risk-bearing | Visible in compliance as a known exception | Exemption (with expiresOn) |
| Permanent and structural | Genuinely outside the governed population | Exclusion (notScopes) |
| Actually a rule that’s too strict for everyone | Reflected in the policy itself | Fix the policy/initiative (not an exception) |
| A control you no longer want anywhere | Removed from enforcement | Re-scope or remove the assignment |
Waiver vs Mitigated: the two categories
There are exactly two exemption categories, and the choice is a semantic claim, not a formality. Get it right and your dashboard tells auditors the truth; get it wrong and you either cry wolf or hide a real gap.
| Dimension | Waiver | Mitigated |
|---|---|---|
| What it claims | “Known non-compliance, accepted for now” | “Control intent met by other means” |
| Is the risk open? | Yes — the gap is real and untreated | No — a compensating control handles it |
| Typical trigger | Legacy app, pending fix, business deadline | Equivalent control at another layer, false positive |
| What an auditor should do | Track it, chase the remediation, watch expiry | Verify the compensating control, then accept |
| Should it expire? | Almost always — it’s temporary by nature | Often, but may be longer-lived if the control is stable |
| Danger if mislabelled | Hides a real, untreated risk as “handled” | Floods the risk register with non-issues |
Use Waiver when the work isn’t done and you’ve decided to live with the gap — for now. The policy is right, the resource genuinely fails it, nobody claims otherwise, and you’re accepting the risk on a timer. The legacy export job needing a public storage account because the partner can’t do private endpoints yet: Waiver. The deprecated app that won’t get TLS 1.2 because it’s being decommissioned in Q3: Waiver. A Waiver tells the auditor “yes, real gap, here’s the ticket and the date we expect it closed.” A Waiver without an expiry is a contradiction — you’ve “accepted the risk for now” with no end to “now.”
Use Mitigated when the policy would flag something that’s actually fine because it’s handled elsewhere. The control’s intent is satisfied by a compensating control at another layer, so the check here is a false positive. A policy demands a resource-level firewall, but the workload sits behind a centralized Azure Firewall enforcing the same restriction more strictly: intent met, so Mitigated. A policy requires a per-resource diagnostic setting, but you collect the same telemetry through a platform-level pipeline: Mitigated. A Mitigated says “don’t chase this — the goal is achieved, just not the way this check looks for.” The discipline that makes it honest is naming the compensating control in the metadata so an auditor can verify the claim rather than take it on faith.
The failure modes are symmetric and both corrosive:
| Mistake | What it looks like | Why it’s harmful | The fix |
|---|---|---|---|
| Labelling a real gap Mitigated | “We’ll call it mitigated to make it look handled” | Hides untreated risk; auditor never chases it | Use Waiver; record the remediation plan |
| Labelling a handled control Waiver | Every false positive becomes “accepted risk” | Risk register fills with non-issues; real ones drown | Use Mitigated; name the compensating control |
| Either category with no expiry | A “temporary” exception with no end | Becomes permanent debt nobody revisits | Always set expiresOn; review before it lapses |
| Either category with no description | A waiver with no “why” | Nobody can later justify or retire it | Mandate a description / linked ticket in metadata |
Scope: where an exemption lives and how far it reaches
An exemption’s scope is the resource container (or single resource) the waiver applies to, and it obeys one iron rule: it must sit at or below the scope of the assignment it exempts, and it only ever subtracts from that assignment’s reach. Get the scope right and the carve-out is surgical; get it too broad and you’ve quietly waived a control for a whole subscription when one resource needed it.
| Exemption scope | Waives the assignment for… | Use when | Risk if chosen carelessly |
|---|---|---|---|
| Management group | Every subscription/resource under that MG | Org-wide structural exception (rare) | Enormous blast radius — waives everything below |
| Subscription | Every RG/resource in that subscription | A whole workload/environment is exempt | Exempts resources you didn’t mean to |
| Resource group | Every resource in that RG | An app’s resources share one exception | Future resources in the RG inherit the waiver |
| Single resource | Just that one resource | One specific offender (the common case) | Tightest, safest — prefer this by default |
Default to the single-resource scope and widen only with a reason. The instinct to exempt a resource group “to be safe” is exactly backwards: an RG-scoped exemption silently covers every current and future resource in that group, so the account you exempt today is joined next month by three more that inherit the waiver without anyone deciding they should. If only one resource has the exception, scope to that one resource. Widen to RG or subscription only when the exception genuinely belongs to the whole container (e.g. a whole non-production subscription waived from a production-grade tagging initiative), and write why in the description.
The exemption scope must be reachable by the assignment, and assignmentScopeValidation enforces it. You cannot exempt a scope the assignment doesn’t cover — exempting a resource group from an assignment on a different subscription does nothing, because the assignment never reached that RG. The assignmentScopeValidation property defaults to validating exactly this: the platform checks your exemption scope falls within the assignment’s effective scope and rejects mis-scoped ones. A DoNotValidate option exists for unusual cases (e.g. creating an exemption ahead of an assignment), but turning validation off is a foot-gun — it lets you create exemptions that point at nothing, sitting in inventory looking meaningful while doing nothing. Leave validation on unless you have a documented reason.
A worked example of how scope flows down. Suppose a “deny public IP on NICs” policy is assigned at the mg-corp management group:
| You create an exemption at… | Effect |
|---|---|
mg-corp (the assignment’s own scope) |
Every NIC under the entire org is exempt — almost certainly wrong |
sub-platform (a child subscription) |
Every NIC in that subscription is exempt — wrong unless the whole sub needs it |
rg-breakglass (one RG) |
Every NIC in that RG is exempt — fine if the RG is purpose-built for break-glass |
nic-jumpbox-01 (one resource) |
Only that NIC is exempt — the surgical, correct choice for one jump box |
rg-in-a-different-MG |
No effect — outside the assignment’s reach; rejected by scope validation |
Expiry: the field that separates discipline from rot
expiresOn is the most important optional field in the entire feature, and the word “optional” is the danger. An exemption with no expiry is a permanent hole that outlives the incident that created it, the engineer who created it, and often the workload itself. Treat it as mandatory by team policy.
expiresOn is a UTC timestamp, and crossing it ends the exemption’s effect, not its existence. You set it to an ISO-8601 UTC time (e.g. 2026-12-31T23:59:59Z). Until that instant, the scope is Exempt. At the next evaluation cycle after it passes, evaluation resumes: the resource is judged again, and if it still fails it flips back to Non-compliant — and if the effect is Deny, new or updated resources at the scope are blocked again as if the exemption never existed. The object doesn’t delete itself; it remains as an expired exemption (the portal flags it; it appears in listings) that you should remove as housekeeping.
Behaviour at and after expiresOn |
What actually happens |
|---|---|
| The instant it passes | Nothing immediate — exemptions act on the next evaluation cycle, not in real time |
| Next evaluation cycle | The scope is evaluated against the assignment again as if un-exempted |
| If still non-compliant | Compliance flips back to Non-compliant |
| If the effect is Deny | New/updated resources at the scope are blocked again |
| The exemption object | Remains as an expired exemption (not auto-deleted) — clean it up |
| Re-evaluation timing | Standard policy evaluation runs roughly every 24 hours; trigger sooner with an on-demand scan |
There is no built-in “warn me before it expires” — you build that. Azure does not email you when an exemption is about to lapse. A forgotten “temporary” exemption silently re-arms a Deny policy and breaks a deploy on the day it expires. The discipline is to query for exemptions nearing expiry on a schedule (Resource Graph or a scheduled script) and review them before the date — renew with fresh justification if still needed, or let it lapse on purpose to force the fix. The expiry isn’t a nuisance; it’s the forcing function that keeps exceptions from becoming permanent.
The expiry-hygiene rules that keep this clean:
| Rule | Why |
|---|---|
Set expiresOn on every exemption |
An exception with no end becomes permanent debt |
| Keep windows short for Waivers (weeks–months) | A Waiver is accepted risk — it should pressure the fix |
| Allow longer windows for Mitigated (if the control is stable) | The intent is genuinely met; less urgency, but still review |
| Alert on exemptions within N days of expiry | Azure won’t warn you — build it (Resource Graph + schedule) |
| Remove expired exemptions as housekeeping | They linger and clutter inventory; an expired one isn’t doing its job |
| Renew deliberately, with fresh justification | Re-creating it forces someone to re-affirm the exception still applies |
How exemptions interact with effects
An exemption suppresses evaluation, which means it touches different effects in different ways. This is where “I exempted it but the remediation still didn’t run” and “I exempted it but the deploy is still blocked” come from. The matrix:
| Effect | What an exemption does to it | The practical consequence |
|---|---|---|
| Deny | Stops the deny from blocking writes at the exempt scope | Resources that would be blocked can now be created/updated |
| Audit | Stops the non-compliant finding for the exempt scope | The resource shows Exempt, not non-compliant |
| AuditIfNotExists | Suppresses the “related thing is missing” finding | No non-compliant flag for the missing related resource |
| Modify | The modification is not applied at the exempt scope | Tags/properties the policy would set are not set |
| DeployIfNotExists | The remediation deployment does not run for the exempt scope | The thing DINE would deploy (a diagnostic setting, an agent) is not deployed |
| Append | The appended value is not added at the exempt scope | The field the policy would add is left as-is |
| Manual | The attestation state is effectively waived for the scope | Shows Exempt rather than requiring attestation |
The DeployIfNotExists trap is the one that quietly hurts. People think of exemptions only as “stop blocking me” (Deny) or “stop nagging me” (Audit). But a DeployIfNotExists policy creates things — a diagnostic setting, the Azure Monitor agent, a backup configuration — and an exemption on its scope means that deployment never happens. Exempt a subscription from a DINE policy that deploys diagnostic settings, and resources there silently stop getting their diagnostics wired up. You wanted to skip a check; you actually skipped a deployment. When you exempt a scope, enumerate the assignment’s effects and ask “am I okay with the Modify and DINE actions also not happening here?” — often you’re not, and the right move is a tighter policyDefinitionReferenceIds that waives only the Audit/Deny control.
Exemptions do not retroactively undo what Modify/DINE already did. If a Modify policy already added a tag, or a DINE policy already deployed a diagnostic setting, an exemption afterward does not remove it — it only stops future enforcement. Exemptions are forward-looking suppression, not a rollback. To undo an already-applied action you change the resource yourself; the exemption just stops the policy from re-applying it.
Targeting specific controls in an initiative
When the assignment is an initiative, an exemption’s default behaviour — waiving the entire set — is almost always too broad. policyDefinitionReferenceIds is the field that makes initiative exemptions surgical, and using it well is the difference between “exempt one app from one HTTPS check” and “accidentally exempt one app from your whole forty-control security baseline.”
Every definition inside an initiative has a policyDefinitionReferenceId. When you build an initiative, each member definition gets a reference ID (a string you choose, like requireHttpsOnly, or a GUID) that uniquely identifies it within that initiative; you find these in the initiative’s policyDefinitions array. An exemption’s policyDefinitionReferenceIds is a list of those IDs: include the ones to waive, and the exemption applies only to those, leaving the rest enforced.
policyDefinitionReferenceIds value |
Effect on the scope |
|---|---|
| Omitted / empty | The entire initiative is exempt for the scope — rarely what you want |
["requireHttpsOnly"] |
Only the HTTPS-only control is waived; the other definitions still apply |
["requireHttpsOnly", "requireTls12"] |
Those two controls are waived; everything else still applies |
| A reference ID that doesn’t exist in the initiative | The exemption matches nothing for that ID (typo = silent no-op) |
The silent-typo failure cuts both ways. Misspell a reference ID and the exemption applies to nothing (no error for the bad ID) — so the control you meant to waive stays enforced, your deploy stays blocked, and you waste an hour wondering why the exemption “didn’t work.” Conversely, omit the list intending to waive one control and you’ve waived the whole initiative — far worse, because forty controls just went Exempt and your dashboard quietly went green on things still non-compliant. Always pull the exact reference IDs from the initiative definition, and after creating the exemption confirm that only the intended controls show Exempt.
To get the reference IDs reliably, read them straight off the initiative:
# List the reference IDs and the underlying definitions in an initiative (policy set)
az policy set-definition show --name "security-baseline" \
--query "policyDefinitions[].{ref:policyDefinitionReferenceId, def:policyDefinitionId}" -o table
Then build the exemption against just those IDs (shown in the lab below). The rule of thumb: an exemption on an initiative assignment should almost always carry a non-empty policyDefinitionReferenceIds. Reserve the whole-initiative exemption for the rare case where a scope genuinely needs relief from all of it.
Architecture at a glance
Picture the policy world as a tree and an exemption as a small, dated label you pin onto one branch. At the top sits your management-group hierarchy; an assignment (a deny policy, or a forty-control initiative) attaches at some node and its enforcement flows downward to every subscription, resource group, and resource beneath. Evaluation is constant: every resource under the assignment is continuously judged into one of a few compliance states — Compliant, Non-compliant, Exempt, Conflicting, or Not started.
An exemption is a separate object (Microsoft.Authorization/policyExemptions) pinned onto a node at or below the assignment’s node. It names the assignment it modifies, carries a category (Waiver or Mitigated), optionally lists the specific reference IDs within an initiative it applies to, and optionally carries an expiresOn. Conceptually it draws a small box around its sub-tree and tells the engine “for this assignment — and, if listed, only these controls — don’t judge what’s in this box; mark it Exempt.” Everything outside is judged normally; siblings of the exempt branch are untouched.
Now walk a request through it. A NIC is created in rg-breakglass. The engine sees it’s under mg-corp, where “deny public IP” is assigned, so the deny would fire and block the create. But there’s an exemption pinned at rg-breakglass, category Waiver, expiresOn three weeks out, scoped to this assignment — so the deny is suppressed, the NIC is created, and it shows as Exempt (not compliant, not blocked, exempt, with category and reason attached). Three weeks later the clock passes expiresOn; on the next cycle the box dissolves, the NIC is judged again, the deny re-arms, and any new public NIC in that RG is blocked once more while the existing one flips to Non-compliant. That dashed, dated box — narrow in scope, honest in category, finite in time — is the whole mental model. An exclusion, by contrast, is a hole cut out of the tree: no box, no label, no date; the branch simply isn’t part of the assignment and nothing on it ever shows up to be reasoned about.
Hold the three box-defining properties together: scope sets how big it is, category sets what it means, expiresOn sets how long it lasts. Lose any one and you lose the discipline — too-wide scope waives too much, a mis-categorised box lies to the auditor, an expiry-less box never goes away.
Real-world scenario
Northwind Logistics ran a CAF-aligned landing zone: a platform team owned a Security-Baseline initiative — 38 controls including deny-public-storage, require-https-only, audit-untagged, enforce-allowed-regions, and a DeployIfNotExists that wired diagnostic settings to a central Log Analytics workspace — assigned at the mg-corp management group. It enforced cleanly for a year. Then the Partner-Integrations team hit a wall: a legacy EDI export had to write to a public storage account because a freight partner’s ancient system couldn’t consume a private endpoint, and the rollout deadline was two weeks out. The deny blocked every deploy.
The on-call engineer’s first instinct was the fast fix: drop rg-edi-export into the assignment’s notScopes. It worked instantly — the deploy went green — and it was exactly wrong. The RG vanished from the dashboard; the public account looked identical to a compliant one; there was no record that security had reviewed and accepted it; and there was no expiry, so when the partner did migrate to a private endpoint nine months later, nobody removed the exclusion. The RG silently stayed outside the baseline — including outside the other 37 controls — so when an engineer later spun up an untagged, diagnostics-less VM in it, no policy said a word.
The governance lead caught it in a quarterly review and replaced it with a policy exemption, with four deliberate choices. Scope: the single storage account, not the RG — so the rest of rg-edi-export snapped back under all 38 controls immediately (and that untagged VM lit up red). Category: Waiver — accepted risk, not a compensating control; the account really was public. Reference IDs: policyDefinitionReferenceIds set to just denyPublicStorage, leaving the other 37 — including the diagnostic-settings DINE — fully enforced on that very account, so it still got its diagnostics wired up. Expiry: expiresOn 90 days out, tied to the partner’s committed migration date, with a linked change ticket.
The payoff showed three months later. A scheduled Resource Graph query flagged the exemption as expiring in 7 days; the partner had migrated, the public account was gone, and the team let the exemption lapse instead of renewing it. The dashboard never lied: the account showed Exempt (with category and ticket) the whole time it existed, then the exemption expired into nothing because the resource it covered was already retired. One over-broad exclusion that hid a public account and 37 unrelated controls for nine months, swapped for one surgical, categorised, 90-day exemption that hid exactly one control on one resource and announced itself before it lapsed. The fix was never “weaken the baseline” — it was “carve the exception narrowly, honestly, and on a timer.”
Advantages and disadvantages
| Advantages | Disadvantages |
|---|---|
| Exception stays visible as Exempt (not hidden) | Adds objects to manage; un-reviewed, they accumulate |
| Category records why (Waiver vs Mitigated) | Mislabelling a category corrupts the risk picture |
expiresOn forces a natural review/retirement |
Expiry is optional — easy to omit and create permanent debt |
| Narrow scope (down to one resource) limits blast radius | Over-broad scope (RG/sub) silently covers future resources |
policyDefinitionReferenceIds waives only the needed controls |
Omit it on an initiative and you waive everything |
| Keeps the policy/initiative unchanged for everyone else | Suppresses Modify/DINE actions, not just checks (surprises) |
| RBAC-gated — you can control who may grant one | Loose RBAC lets teams self-exempt their own non-compliance |
| Auditable and reportable via Resource Graph | No native “expiring soon” alert — you must build it |
When the advantages matter most: in any enforced (Deny/Modify/DINE) governance program at scale, where exceptions are inevitable and you need them tracked rather than hidden. The visibility and category turn “we have exceptions” from a liability into a defensible, auditable position. When the disadvantages bite: when exemptions are created ad hoc with no expiry, no description, over-broad scope, and loose RBAC — at which point they become a second shadow governance system that quietly unwinds the first. The feature is only as good as the discipline around expiresOn, scope, and who may write them.
Hands-on lab
A free-tier-friendly walk-through: assign an audit policy (so nothing is blocked or costs money), watch a resource go non-compliant, exempt it as a Mitigated carve-out with an expiry, confirm it shows Exempt, then tear down. Audit effect means this is safe to run in any subscription.
1. Set variables and pick a resource group.
RG="rg-exemption-lab"
LOC="centralindia"
SUB=$(az account show --query id -o tsv)
az group create --name "$RG" --location "$LOC"
2. Assign a built-in audit policy that flags storage accounts allowing public blob access. (Built-in definition: “Storage account public access should be disallowed” — an Audit policy, nothing is blocked.)
# Look up the built-in definition id by display name
DEF_ID=$(az policy definition list \
--query "[?displayName=='Storage account public access should be disallowed'].id | [0]" -o tsv)
# Assign it at the resource group scope (audit only)
az policy assignment create \
--name "audit-public-blob" \
--display-name "Audit public blob access (lab)" \
--policy "$DEF_ID" \
--scope "/subscriptions/$SUB/resourceGroups/$RG"
3. Create a storage account that will trip the audit (allowing blob public access), then trigger an on-demand evaluation so you don’t wait ~24h:
SA="stexlab$RANDOM"
az storage account create --name "$SA" --resource-group "$RG" \
--location "$LOC" --sku Standard_LRS --allow-blob-public-access true
# Trigger an on-demand compliance scan for the resource group
az policy state trigger-scan --resource-group "$RG"
After the scan, confirm it’s non-compliant:
az policy state list --resource-group "$RG" \
--query "[?complianceState=='NonCompliant'].{res:resourceId, state:complianceState}" -o table
4. Create the exemption — category Mitigated (pretend a central control handles public-access risk), scoped to just that storage account, with an expiry:
ASSIGNMENT_ID=$(az policy assignment show --name "audit-public-blob" \
--scope "/subscriptions/$SUB/resourceGroups/$RG" --query id -o tsv)
SA_ID=$(az storage account show --name "$SA" --resource-group "$RG" --query id -o tsv)
az policy exemption create \
--name "exempt-lab-storage" \
--display-name "Lab storage exemption (Mitigated)" \
--policy-assignment "$ASSIGNMENT_ID" \
--exemption-category Mitigated \
--scope "$SA_ID" \
--expires-on "2026-12-31T23:59:59Z" \
--description "Public-access risk handled by central network control; lab demo. Ticket GOV-1234."
5. Re-scan and confirm the resource now shows Exempt rather than non-compliant:
az policy state trigger-scan --resource-group "$RG"
az policy state list --resource-group "$RG" \
--query "[].{res:resourceId, state:complianceState}" -o table
# The storage account's state should now read 'Exempt'
You can also list the exemption and read its category and expiry back:
az policy exemption list --scope "$SA_ID" \
--query "[].{name:name, category:exemptionCategory, expires:expiresOn}" -o table
6. Bicep equivalent of the exemption (for the IaC path — note the symbolic reference to the assignment and the scope via scope):
// Exemption authored as code, scoped to a single resource (the storage account)
resource exemption 'Microsoft.Authorization/policyExemptions@2022-07-01-preview' = {
name: 'exempt-lab-storage'
scope: storageAccount // any resource/RG/sub/MG symbol or extension scope
properties: {
displayName: 'Lab storage exemption (Mitigated)'
policyAssignmentId: auditAssignment.id
exemptionCategory: 'Mitigated' // 'Waiver' or 'Mitigated'
expiresOn: '2026-12-31T23:59:59Z' // UTC; omit = permanent (avoid)
assignmentScopeValidation: 'Default' // keep validation on
// For an INITIATIVE assignment, waive only specific controls:
// policyDefinitionReferenceIds: [ 'denyPublicStorage' ]
description: 'Public-access risk handled by central network control. Ticket GOV-1234.'
}
}
7. Teardown (removes the exemption, assignment, and the whole RG):
az policy exemption delete --name "exempt-lab-storage" --scope "$SA_ID"
az policy assignment delete --name "audit-public-blob" \
--scope "/subscriptions/$SUB/resourceGroups/$RG"
az group delete --name "$RG" --yes --no-wait
You’ve now created the full lifecycle: assign → fail → exempt (categorised, scoped, expiring) → verify Exempt → remove. Swap the Audit policy for a Deny one and the only change is that step 3 would be blocked until the exemption in step 4 exists.
Common mistakes & troubleshooting
The failure modes that cost the most time, each as symptom → root cause → how to confirm → fix. These are the ones that turn a five-minute carve-out into a half-day mystery or a silent governance hole.
| # | Symptom | Root cause | Confirm with | Fix |
|---|---|---|---|---|
| 1 | Created an exemption but the resource still shows non-compliant | Evaluation hasn’t re-run yet | az policy state list still old; no recent scan |
az policy state trigger-scan; wait for the cycle |
| 2 | Exemption “did nothing” on an initiative | Wrong/typo’d policyDefinitionReferenceIds |
Diff IDs against az policy set-definition show |
Use exact reference IDs from the initiative |
| 3 | Exempted one control, but the whole baseline went Exempt | Omitted policyDefinitionReferenceIds on an initiative |
Compliance shows all controls Exempt for the scope | Re-create with the specific reference ID(s) |
| 4 | Deploy started failing again “out of nowhere” | The exemption’s expiresOn passed |
az policy exemption list shows it expired |
Renew with fresh justification, or fix the resource |
| 5 | DeployIfNotExists stopped wiring up diagnostics for a scope | Exemption suppressed the DINE action, not just a check | Resources in scope missing the deployed config | Scope the exemption to only the Audit/Deny ref, not DINE |
| 6 | Exemption create rejected / has no effect | Scope is outside the assignment’s reach | assignmentScopeValidation error; wrong sub/MG |
Exempt a scope at/under the assignment’s scope |
| 7 | “Temporary” exemption is two years old | No expiresOn was ever set |
expiresOn is null in the object |
Add an expiry; build an expiring-soon alert |
| 8 | A team waived their own non-compliance | They hold write on exemptions at their scope | az role assignment list shows the grant |
Restrict exemption-write to governance; review grants |
| 9 | Excluded RG silently dropped all controls, not one | Used notScopes (exclusion) instead of an exemption |
Assignment’s notScopes contains the RG |
Remove from notScopes; create a scoped exemption |
| 10 | Auditor flags “handled” items as open risk | Real gaps mislabelled, or no description/category rationale | Exemptions are Waiver with no compensating note |
Re-categorise honestly; add description/ticket |
The expanded reasoning for the ones that bite hardest:
2 & 3 — the initiative reference-ID pair. Two sides of one coin. A wrong reference ID (typo, or a GUID not in this initiative) matches nothing — the control stays enforced, your deploy stays blocked, and the exemption looks broken. Omitting the list on an initiative waives all of it — the deploy unblocks but thirty-nine other controls just went Exempt, which is more dangerous because it looks like success. Always read the exact IDs off az policy set-definition show, set them explicitly, and verify afterward that only the intended controls show Exempt.
4 & 7 — the expiry pair. Symptom 4 (“deploy broke for no reason”) is almost always a forgotten expiresOn that passed — confirm with az policy exemption list --query "[?expiresOn < '<now>']" and either renew it deliberately or fix the resource. Symptom 7 is the opposite rot: no expiry was ever set, so a two-week carve-out is now a two-year hole. Both fixes point the same way: every exemption gets an expiresOn, and you run a scheduled query for ones nearing it.
5 — the DeployIfNotExists suppression. The subtle one. You exempted a scope to silence an audit, but the same assignment also carries a Modify or DINE effect, and the exemption suppressed those actions too — so diagnostic settings, agents, or tags the policy would deploy silently stopped. Confirm by checking whether resources in the scope are missing the thing DINE deploys. Fix by scoping the exemption (via policyDefinitionReferenceIds) to only the Audit/Deny definition, leaving the remediating one enforced.
8 — self-service exemptions. If application teams hold write on policyExemptions at their own scope, they can exempt themselves from the controls meant to govern them — a self-graded exam. Confirm with az role assignment list --scope <scope> looking for Microsoft.Authorization/policyExemptions/write (often via Resource Policy Contributor or inherited Owner/Contributor). Fix by reserving exemption-write to a governance group and routing exception requests through review.
To find every exemption (including expired) across the estate, Resource Graph is the fastest lens:
policyresources
| where type == "microsoft.authorization/policyexemptions"
| extend cat = tostring(properties.exemptionCategory),
expires = todatetime(properties.expiresOn),
assignment = tostring(properties.policyAssignmentId)
| project name, scope=id, cat, expires, assignment
| order by expires asc
Run that on a schedule and you have the “what have we waived, why, and when does it lapse” report that auditors and your future self will thank you for.
Best practices
- Reach for an exemption only when it’s the right tool. A permanent structural carve-out is an exclusion (
notScopes); a rule that’s wrong for everyone is a policy change; a known, scoped, temporary exception is an exemption. Don’t use one for another’s job. - Set
expiresOnon every single exemption. The platform makes it optional; your standards must make it mandatory. A no-expiry exemption is permanent governance debt by default. - Scope as tightly as the exception demands — default to the single resource. Widen to RG or subscription only when the whole container genuinely shares the exception, and write why in the description. Remember an RG scope silently covers future resources.
- Categorise honestly: Waiver = accepted open risk; Mitigated = handled elsewhere. Name the compensating control in the description for every Mitigated, and link the remediation ticket for every Waiver.
- On an initiative, always set
policyDefinitionReferenceIds. Waive only the controls that need it; never let a one-control exception silently exempt the whole baseline. Pull the exact IDs from the initiative definition. - Always write a description with a reason and a ticket. “Why does this exception exist and who approved it” must live on the object, not in someone’s memory.
- Gate exemption-write behind a governance role. Route exception requests through review; don’t let teams self-exempt the controls that govern them.
- Build an “expiring soon” report. Azure won’t warn you — schedule a Resource Graph query (the KQL above) and review exemptions before they lapse.
- Prefer exemptions over exclusions for anything risk-bearing. If you ever want to report the exception, it must be an exemption; an exclusion is invisible by design.
- Author exemptions as code (Bicep) and review them in PRs. A carve-out to a security control deserves the same scrutiny as the control itself — diffable, reviewable, with the justification in the commit.
- Re-evaluate at renewal, not just at creation. When an exemption nears expiry, force a decision: is it still needed? Renew with fresh justification or let it lapse to drive the fix.
- Check the effect mix before exempting. If the assignment includes Modify/DINE, confirm you’re okay with those actions not running at the scope — usually you want a reference-scoped exemption that spares the remediating control.
Security notes
- Exemption-write is a privileged action — treat it as one. Creating an exemption can suppress a security control (deny-public-storage, require-https, enforce-encryption). The RBAC permission is
Microsoft.Authorization/policyExemptions/write, held by the built-in Resource Policy Contributor role (and by Owner). Grant it to a small governance group, not broadly, and never to the teams whose resources the policy governs — self-exemption defeats the control. - An exemption can never grant more than the granter holds at the scope. The platform’s self-elevation guard means you cannot create an exemption that waives a policy your own role couldn’t otherwise influence at a broader scope; exemptions narrow assignments, they don’t escalate privilege. Still, review who has the write permission at management-group scope, where one exemption waives the most.
- Keep
assignmentScopeValidationon. Leaving it atDefaultensures an exemption can only target a scope the assignment actually reaches;DoNotValidatelets you create exemptions pointing at nothing, which clutter inventory and can mask a mis-scoped intent. UseDoNotValidateonly with a documented reason. - Don’t let exemptions become a backdoor around security baselines. A Mitigated exemption that claims a compensating control must name and link that control so it can be verified; an unverifiable “Mitigated” is just an unaudited hole. Require the description.
- Watch the Defender for Cloud interaction. Many secure-score recommendations are backed by policy; an exemption changes how a resource is counted. Exempting a control can move a resource out of the failing set and raise secure score without the underlying risk being addressed — so an exemption used to “fix” a score is a lie to your own posture metric. Audit exemptions when reconciling score against reality (see Secure Score Won’t Budge: Diagnosing Stuck Recommendations).
- Audit the audit trail. Exemption create/update/delete are control-plane operations in the Activity Log — alert on exemption creation at sensitive scopes so a new carve-out to a security baseline is never silent.
- Expiry is a security control, not just hygiene. A Waiver that outlives its risk acceptance is an un-renewed acceptance — i.e. nobody currently owns that risk. The forcing function of
expiresOnkeeps risk acceptance current and owned.
Cost & sizing
Exemptions themselves are free — Microsoft.Authorization/policyExemptions objects carry no charge, and neither does Azure Policy evaluation. The cost dimension here is not rupees; it’s governance overhead and risk cost, and they’re real even if they don’t appear on an invoice.
- The “cost” is review effort. Every exemption is an object someone must create with a justification, categorise correctly, scope, expiry-set, and re-review at renewal. A landing zone with dozens of exemptions across teams needs a lightweight process (request → review → create-as-code → scheduled expiry check). Skipping that process is “free” today and expensive at the next audit.
- The hidden risk cost of getting it wrong. An over-broad exclusion (used where an exemption belonged) or a no-expiry exemption is a latent breach/audit-finding waiting to surface. The Northwind case turned one fast
notScopesedit into nine months of 37 unmonitored controls — no line item, but a genuine cost when discovered. - Tooling cost is negligible. The “expiring soon” report is a Resource Graph query on a schedule (Azure Automation runbook, a scheduled GitHub Action, or a Logic App) — pennies. There’s no SKU to size; if anything, exemptions reduce cost by preventing the over-corrective alternative (e.g. teams disabling whole assignments, then re-building governance after an incident).
Rough sense of the operational footprint, since there’s no bill to right-size:
| “Cost” dimension | What drives it | How to keep it small |
|---|---|---|
| Exemption objects | Number of live exceptions | Tight scope + expiry so they self-retire |
| Review overhead | Exemptions created without process | Request→review→IaC pipeline; PR-reviewed |
| Expiry monitoring | No native alerting | One scheduled Resource Graph query (negligible) |
| Audit/risk exposure | Mis-categorised or no-expiry exemptions | Mandate category + description + expiresOn |
| Azure charges | — | None — exemptions and evaluation are free |
Interview & exam questions
1. What is the difference between a policy exemption and an exclusion (notScopes)? An exemption keeps the resource in scope but suppresses evaluation, marking it Exempt with a category and (ideally) an expiry — the exception stays visible and accountable. An exclusion removes the scope from the assignment via notScopes, so the resource is never evaluated and reports compliant-by-omission with no record. Use exemptions for tracked, temporary, or risk-bearing exceptions; exclusions only for permanent structural carve-outs.
2. What are the two exemption categories and when do you use each? Waiver and Mitigated — the only two values. Waiver means accepted, known non-compliance (a real, untreated gap, usually temporary) — use it when the work isn’t done and you’re living with the risk on a timer. Mitigated means the control’s intent is met by a compensating control elsewhere, so the check is a false positive — use it when the goal is genuinely achieved another way, and name that control.
3. An exemption is assigned at a resource group, but the policy is assigned at a different subscription. What happens? Nothing — it has no effect because its scope is outside the assignment’s reach. An exemption must target a scope at or below the assignment’s scope, and assignmentScopeValidation (default on) rejects or flags a mis-scoped exemption. Exemptions only narrow an assignment’s reach; they can’t reach a scope the assignment never covered.
4. You exempt a subscription from an initiative to silence one audit finding, and diagnostic settings stop getting deployed. Why? The initiative includes a DeployIfNotExists effect, and exempting the whole initiative suppressed that remediation action, not just the audit check — so DINE stopped deploying the diagnostic settings. The fix is to set policyDefinitionReferenceIds to only the audit/deny definition you meant to waive, leaving the DINE control enforced. Exemptions suppress all effects of whatever they cover.
5. What does expiresOn do, and what happens the moment it passes? It’s a UTC timestamp after which the exemption stops applying. On the next evaluation cycle after it passes, the scope is judged against the assignment again: if still non-compliant it flips to Non-compliant, and a Deny effect blocks new writes again. The exemption object isn’t deleted — it remains as an expired exemption to clean up — but its effect ends. There’s no native “expiring soon” alert; you must build one.
6. How do you exempt only one control inside a 40-control initiative, not the whole thing? Set the exemption’s policyDefinitionReferenceIds to the specific reference ID(s) of the definition(s) to waive — pulled exactly from the initiative definition’s policyDefinitions array. Omitting the list waives the entire initiative for that scope (a common, dangerous mistake). A typo’d reference ID silently matches nothing, leaving the control enforced.
7. Who can create a policy exemption, and why does that matter for security? Anyone with Microsoft.Authorization/policyExemptions/write at the scope — the built-in Resource Policy Contributor role, or Owner. It matters because an exemption can suppress a security control, so if application teams hold that permission at their own scope they can self-exempt the controls meant to govern them. Reserve exemption-write to a governance group and route requests through review.
8. Does creating an exemption undo a Modify or DINE action that already ran? No. Exemptions are forward-looking suppression — they stop future enforcement at the scope but do not remove a tag a Modify policy already added or delete a diagnostic setting a DINE policy already deployed. To undo an already-applied action you change the resource yourself; the exemption only stops the policy from re-applying it.
9. Why is an exemption better than lowering the policy to Audit or editing allowedValues for a one-off exception? Both change behaviour for the entire assignment/definition — every scope under it — not one resource, and leave no record on the resource that needed the exception. An exemption affects only the scope you set, keeps the policy intact for everyone else, and records category, reason, and expiry. The blast radius and auditability are completely different.
10. How do you find every exemption in your estate and which are about to expire? Query Azure Resource Graph for type == "microsoft.authorization/policyexemptions", projecting exemptionCategory, expiresOn, and the assignment, ordered by expiresOn. Run it on a schedule (Automation runbook, scheduled Action, or Logic App) for the “what’s waived, why, and what lapses soon” report, since Azure provides no built-in expiry alert.
11. In the dashboard, what does the Exempt state tell you versus Non-compliant or “not evaluated”? Exempt means the resource is governed but has a documented, tracked waiver (category and, ideally, expiry) — a known, accountable exception. Non-compliant means governed and currently failing with no waiver. A resource that’s simply not evaluated (excluded via notScopes) shows neither — it’s invisible, which is exactly why exclusions hide exceptions and exemptions surface them.
12. What’s the risk of an exemption with no expiresOn? It’s permanent — it never reviews itself, never pressures the underlying fix, and outlives the incident and the people who created it. For a Waiver that’s an un-renewed risk acceptance (nobody currently owns the risk); for any exemption it’s debt that quietly unwinds a control forever. Mandate expiresOn even though the platform makes it optional.
These map to AZ-104 (Administrator) — implement and manage Azure governance: policy, initiatives, exemptions, and compliance — and especially AZ-500 (Security Engineer) — manage security policies and governance, and reason about exceptions to security baselines. The compensating-control reasoning behind Mitigated, and the Defender-for-Cloud secure-score interaction, are squarely AZ-500 territory. A compact cert-mapping for revision:
| Question theme | Primary cert | Objective area |
|---|---|---|
| Exemption vs exclusion, Exempt state | AZ-104 | Implement and manage governance |
| Waiver vs Mitigated, compensating controls | AZ-500 | Manage security policies / exceptions |
Scope rules, assignmentScopeValidation |
AZ-104 | Policy scope and assignment |
policyDefinitionReferenceIds on initiatives |
AZ-104 | Initiatives and compliance |
| RBAC for exemption-write, self-exemption risk | AZ-500 | Least-privilege governance |
| Secure-score interaction | AZ-500 | Defender for Cloud posture |
Quick check
- Your teammate added a resource group to a policy assignment’s
notScopesto “exempt” it. Why is that not the same as a policy exemption, and what’s the downside? - A control in a 38-definition initiative needs waiving for one app, but after you create the exemption the whole baseline shows Exempt for that scope. What did you forget?
- You set up a “temporary” exemption six weeks ago and today a deploy started failing again with a Deny. What most likely happened, and what are your two options?
- When should you choose Mitigated over Waiver, and what must you record to keep Mitigated honest?
- True or false: an exemption created at a management group can waive a policy that’s assigned at a child subscription.
Answers
notScopesis an exclusion — it removes the RG from the assignment so it’s never evaluated and reports compliant-by-omission, with no category, reason, or expiry. A policy exemption keeps the RG in scope but marks it Exempt (tracked, categorised, expiring). The downside of the exclusion: the exception is invisible — indistinguishable from genuine compliance — and it silently drops all the assignment’s controls for that RG, including ones unrelated to the actual exception.- You omitted
policyDefinitionReferenceIds. On an initiative assignment, an exemption with no reference IDs waives the entire initiative for the scope. Re-create it with the specific reference ID(s) of just the control you meant to waive, pulled from the initiative definition, and verify only that control shows Exempt. - The exemption’s
expiresOnpassed, so the policy re-armed and the Deny is blocking again. Confirm withaz policy exemption list(it shows as expired). Two options: renew the exemption with fresh justification if the exception still applies, or fix the underlying resource so it’s compliant and let the exemption stay lapsed. (And build an expiring-soon alert so it’s never a surprise next time.) - Choose Mitigated when the control’s intent is genuinely met by a compensating control elsewhere (the check here is a false positive), rather than when you’re simply accepting an open risk (that’s Waiver). To keep Mitigated honest, record the compensating control in the exemption’s description so an auditor can verify the claim — an unverifiable “Mitigated” is just an unaudited hole.
- False. An exemption can only target a scope at or below the assignment’s scope — it narrows an assignment, never widens it. A management-group-scoped exemption sits above a child subscription’s assignment, so it can’t waive it; you’d exempt at the subscription, RG, or resource under that assignment instead.
Glossary
- Policy exemption — a tracked object (
Microsoft.Authorization/policyExemptions) that suppresses a specific assignment for a specific scope, marking it Exempt; carries a category and optional expiry. - Exclusion (
notScopes) — a scope listed on the assignment itself so it’s removed from evaluation entirely; the resource is never judged and reports compliant-by-omission. exemptionCategory— the reason an exemption exists; exactly one ofWaiver(accepted open risk) orMitigated(control intent met by a compensating control).- Waiver — exemption category meaning known, accepted, usually-temporary non-compliance with an untreated gap.
- Mitigated — exemption category meaning the control’s intent is satisfied by another means (a compensating control), so the check here is a false positive.
- Scope (of an exemption) — the management group, subscription, resource group, or single resource the exemption applies to; must be at/under the assignment’s scope.
expiresOn— UTC timestamp after which an exemption stops applying; on the next evaluation cycle the scope is judged again. Optional in schema, mandatory by good practice.policyDefinitionReferenceIds— list of reference IDs identifying which definitions inside an initiative the exemption waives; omit it and the whole initiative is exempted.policyDefinitionReferenceId— the unique identifier a member definition has within an initiative; found in the initiative’spolicyDefinitionsarray.- Exempt (compliance state) — the dashboard state of a resource covered by an exemption: in scope, waived, visible, and accountable (not the same as compliant or not-evaluated).
assignmentScopeValidation— exemption property (defaultDefault) that checks the exemption scope falls within the assignment’s reach;DoNotValidatedisables the check.- Compensating control — an alternative safeguard that meets a policy’s intent by other means; the justification that makes a Mitigated exemption legitimate.
- Resource Policy Contributor — built-in RBAC role holding
Microsoft.Authorization/policyExemptions/write; the permission gate for granting exemptions. - DeployIfNotExists (DINE) — a policy effect that deploys a remediating resource; an exemption suppresses its action, not just a check — the classic exemption surprise.
- Compliance state — one of Compliant, Non-compliant, Exempt, Conflicting, or Not started, assigned per resource per assignment.
Next steps
You can now grant a carve-out that’s narrow, honest, and on a timer — and tell it apart from the exclusions and policy edits that hide exceptions instead of tracking them. Build outward:
- Next: Azure Policy Effects Decoded: Deny vs Audit vs Modify vs DeployIfNotExists — know exactly which effects an exemption suppresses (and why DINE is the trap).
- Related: Policy Definitions vs Initiatives: When to Bundle Controls into a Set — initiatives are where
policyDefinitionReferenceIdstargeting matters most. - Related: Azure Policy and Governance at Scale: Enforce the Rules Automatically — the broader program exemptions plug into.
- Related: Management Groups 101: Designing a Hierarchy That Scopes Policy and RBAC — assignment and exemption scope both live on this tree.
- Related: Azure RBAC Fundamentals: Roles, Scopes and Assignments Without the Confusion — who may grant an exemption is itself a control.
- Related: Secure Score Won’t Budge: Diagnosing Stuck Recommendations and Assessment Drift — how exemptions move (and can mislead) your secure score.