Enforcement strategy¶
The AVM deployment lands a deliberately soft policy posture so it never blocks an existing estate on day one. Hardening it for production is a defined, phased exercise, not a single switch. This page is the method.
Read Azure Policy for the model and Customizing policies for the mechanics. This page is about when and in what order to enforce.
What the deployment lands, and why¶
Straight after avm-ptn-alz runs, the assignments sit in three postures:
| Posture | What it is | Examples |
|---|---|---|
Report-only (Audit/AuditIfNotExists) |
Flags non-compliance, blocks nothing | Microsoft cloud security benchmark, zone-resiliency and unused-resource audits |
Staged off (enforcementMode = DoNotEnforce) |
Assigned but inert, ready to switch on | The whole Enforce-Guardrails (Enforce-GR-*) per-service set, CMK enforcement, private-subnet |
Already enforced (Deny/DeployIfNotExists, Default) |
Blocks or auto-remediates now | Deny public IP, deny management ports from internet, subnet-without-NSG, AKS hardening, diagnostic and Defender deployment |
The soft default is intentional. Microsoft's own guidance is to "start with a
policy overlay (audit mode) and measure impact before introducing deny effects".
The large Enforce-Guardrails set ships in DoNotEnforce precisely so you can turn
it on progressively once you know what it would break.
Two other defaults worth knowing: the Defender for Cloud config assignment ships
with most plans disabled (turn on the ones you want, they carry cost), and
DeployIfNotExists policies do not fix resources that already exist until you
run a remediation task.
Two levers, kept separate¶
| Lever | Changes | Reach for it to |
|---|---|---|
Effect (Audit → Deny, via the effect parameter or an override) |
Whether a control blocks or only reports | Harden one guardrail |
enforcement_mode (Default ↔ DoNotEnforce) |
Whether the assignment fires at all, without touching the effect | Safe-deployment "what-if"; switch a staged-off assignment on |
DoNotEnforce is the "what-if" mode: the effect never triggers and, importantly,
it writes no Activity Log entries, so alerting on non-compliance stops while an
assignment is muted. For DeployIfNotExists you can still run manual remediation
in this mode.
The rollout method: safe deployment of policy¶
Microsoft's Safe Deployment of Policy (SDP) flow is the pattern for turning any control on without a tenant-wide surprise. For each control:
- Keep it broad but scope it to the least-critical tier first with a
resource_selectorsblock (byresourceLocation), and setenforcement_mode = DoNotEnforce. - Let the compliance scan complete and validate the result: no false positives, and the expected resources flagged. For DINE/Modify, run the remediation task on pre-existing resources.
- Switch that tier to
enforcement_mode = Default(and/or effect toDeny). - Expand the
resource_selectorsto the next tier and repeat, ending with production.
For the DINE/Modify automation, the same idea is framed as three phases: observe in
DoNotEnforce, enable on a reduced scope (a sandbox or one non-prod subscription),
then enable broadly. Test policy changes in a canary management-group hierarchy
in the same tenant before they reach production.
Recommended production baseline¶
The landing-zone unit ships two commented reference blocks in
_foundation/landing-zones/main.tf, both inert until you lift entries into
policy_assignments_to_modify: prod_hardening (the enforcement recommendation
below) and common_customizations (the day-one settings and a reference of every
modify capability). The prod_hardening recommendation, in safe-deployment order:
| Wave | Controls | Notes |
|---|---|---|
| Tag governance | Enforce-Tag-Gov effects Audit → Deny |
Once resource groups carry the baseline tags |
| Wave 1 (data services) | Storage, Key Vault, SQL, MySQL, PostgreSQL, Cosmos DB, Data Explorer, Data Factory, Synapse, Event Hub, Event Grid, Service Bus guardrails DoNotEnforce → Default |
Lowest workload-breakage risk |
| Wave 2 (compute, network, apps) | Compute, Network, Kubernetes, App Services, Container Apps / Instances / Registry, Automation, API Management, Cognitive Services, Machine Learning, OpenAI, Bot Service, Virtual Desktop guardrails → Default |
Validate against running workloads first |
| Private subnets | Enforce-Subnet-Private → Default |
Confirm workload subnets comply |
| Customer-managed keys | Enforce-Encrypt-CMK0 → Default |
Last, and only where per-workload key infrastructure exists |
| Defender for Cloud | Enable the plans you want on Deploy-MDFC-Config |
Cost decision; most plans ship disabled |
Enable each wave scoped to sandbox or one region first (with resource_selectors),
validate, then expand. The same Enforce-Guardrails set also sits at the
platform scope; enforce there once the platform subscriptions are ready.
The common_customizations block covers the settings customers change first:
Microsoft Defender for Cloud (the security contact email, which ships as a
placeholder, and the per-service plans, which ship disabled) and DDoS Protection
(opted out by default). It also documents, as commented examples, every modify
capability used on this page: enforcement_mode, the resource_selectors phased
rollout, not_scopes exclusions, overrides, and non-compliance messages. For a
one-off, time-bound exception on a specific resource, prefer a tracked Azure Policy
exemption over not_scopes.
Differentiate by environment through scope, not per-assignment hacks¶
Where a control should be stricter for production than for sandbox, express that by
management-group scope and archetype, not by special-casing one assignment.
Subscription placement in the hierarchy determines the resultant policy set, so a
stricter assignment on the corp archetype and a lighter one on sandbox is the
idiomatic way. The Enforce-GR-* sets already sit at platform and landingzones
for exactly this reason: enforce them where workloads run, leave sandbox free.
Do not forget remediation¶
Turning enforcement on does not fix what already exists. Resources predating a
Deny are not retroactively rejected, and DeployIfNotExists/Modify do not touch
pre-existing resources until a remediation task runs. That task uses the
assignment's managed identity, which must hold the right RBAC role. Management-group
assignments are remediated after evaluation (portal, or
az policy remediation create).
Exceptions: pick the right instrument¶
| Instrument | Behaviour | Use for |
|---|---|---|
Exclusion (not_scopes) |
Bypasses evaluation for a scope, not tracked | A whole test subscription that should not be governed |
| Exemption | Time-bound, per-resource, still tracked, waiver or mitigated, with metadata and expiry |
A specific, justified, temporary pass |
| Override | Changes the effect for named policies in an initiative without editing the definition | Relaxing one policy inside a set |
Record justification, owner, ticket, and expiry on every exemption, and review them: stale exemptions are governance erosion. Aim for as few assignments and exemptions as possible.
Expressing all of this in the module¶
Every lever above is an input to policy_assignments_to_modify, so it stays in code
and flows through the pull-request pipeline:
policy_assignments_to_modify = {
landingzones = {
policy_assignments = {
# 1. staged-off guardrail: switch on, prod region first
Enforce-GR-Storage0 = {
enforcement_mode = "Default"
resource_selectors = [{
name = "phase-1"
resource_selector_selectors = [{ kind = "resourceLocation", in = ["westeurope"] }]
}]
}
# 2. audit -> deny on a specific effect parameter
Enforce-Tag-Gov = {
parameters = { environmentEffect = jsonencode({ value = "Deny" }) }
}
}
}
}
CAF and WAF mapping¶
- Advisory-first, phased enable = CAF Govern (policy-driven guardrails) and WAF Operational Excellence (staged, reversible change).
- Enforcing in production = WAF Security (prevent, not just detect) and Cost Optimization (block waste and misconfiguration at the door).
- Canary hierarchy and tiered
resource_selectors= WAF Reliability (contained blast radius).