Skip to content

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 (AuditDeny, via the effect parameter or an override) Whether a control blocks or only reports Harden one guardrail
enforcement_mode (DefaultDoNotEnforce) 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:

  1. Keep it broad but scope it to the least-critical tier first with a resource_selectors block (by resourceLocation), and set enforcement_mode = DoNotEnforce.
  2. 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.
  3. Switch that tier to enforcement_mode = Default (and/or effect to Deny).
  4. Expand the resource_selectors to 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.

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).

References