Pick effects deliberately
| Effect | Use for | Risk |
|---|---|---|
| Audit / AuditIfNotExists | Visibility, early rollout, things you can't block | No protection, just reports |
| Deny | Hard rules (allowed regions, no public IPs on NICs in prod) | Blocks deployments; needs clear error messages |
| DenyAction | Protect against specific actions (e.g. delete on critical resources) | Narrow scope |
| Modify | Add/fix tags, settings at create/update | Changes resources silently; document it |
| DeployIfNotExists | Diagnostic settings, private DNS records, agents, backup | Needs managed identity and remediation tasks |
| Disabled | Parameterised off-switch | Easy to forget |
Parameterise the effect in definitions so the same policy can be Audit in one scope and Deny in another.
Assign at the right scope
Tenant Root
└─ Org MG ── baseline (allowed regions, security center, activity log export)
├─ Platform MG ── connectivity/identity/management rules
├─ Landing Zones MG
│ ├─ Corp ── no public endpoints, private DNS
│ └─ Online ── WAF required, public allowed with rules
└─ Sandbox MG ── light rules, budget limits
Assign high, exempt low. Don't assign the same initiative at many subscriptions separately.
Roll out in rings
- Audit in all scopes for 2–4 weeks; review non-compliant resources with owners.
- Fix existing resources (remediation tasks for DINE/Modify; tickets for others).
- Deny in sandbox/dev first, then non-prod, then prod.
- Announce each switch with the date, the rule and how to request an exemption.
az policy remediation create -n fix-diag-settings --management-group mg-landingzones \
--policy-assignment <assignment-id> --definition-reference-id diagSettingsStorage \
--resource-discovery-mode ReEvaluateCompliance
Exemptions
- Category
Waiver(accepted risk) orMitigated(handled another way). - Always set
expiresOnand an owner in metadata/description. - Review expiring exemptions monthly; an exemption without expiry is a permanent hole.
az policy exemption create -n legacy-app-public-ip --policy-assignment <assignment-id> \
--exemption-category Waiver --expires-on 2027-03-31T00:00:00Z \
--scope /subscriptions/<sub>/resourceGroups/rg-legacy --description "Owner: team-x; CHG12345; migrating to Front Door"
Policy as code
- Definitions, initiatives, assignments and exemptions in Git (EPAC, Terraform or Bicep).
- PR review by the platform/security team; pipeline deploys to a test management group first.
what-if/ plan output in the PR shows which scopes change.- No portal edits in prod: drift is detected and reverted by the pipeline.
Make compliance actionable
- Report per owner (via tags), not one big percentage.
- Separate "new non-compliance" (should be zero with Deny) from legacy debt.
- Include remediation guidance in the policy's description and the Deny message (
nonComplianceMessageson the assignment).
Design review questions
- What's the business reason for each Deny rule, and who owns it?
- How does a team request an exception, and how fast?
- Which DINE policies have managed identities with which roles?
- How are built-in initiative updates (new versions) reviewed before adoption?
Tip
Write the Deny message for a stressed engineer at 2 AM: what was blocked, why, and where to read more.
"Public IPs are not allowed in Corp landing zones. Use the internal load balancer pattern: wiki/lz-corp-ingress"saves a ticket.
Checklist
- Effects chosen and parameterised
- Assignments at management group level
- Audit → remediate → Deny rings with announcements
- Exemptions with category, owner and expiry
- Policy as code with PR review
- Compliance reported per owner with guidance