By the end of this you have the mandatory-tag and allowed-location policies assigned to the tree in a non-enforcing mode, trialed safely, and promoted to blocking only after a clean scan; a modify policy inheriting tags from each resource group down onto the resources inside it, with a remediation pass for what already exists; the gateway subnet exempted from the subnet-NSG requirement as a documented mitigated exemption; the crown-jewel resources protected from deletion; and an end-to-end test that proves a non-compliant resource group is blocked and a compliant one is accepted. The portal walk is the teaching spine; a consolidated set of commands reproduces each step. Northfork Supply Co. is the running example, with its single Production subscription under the tree.
Before Step 1: you need Owner on the Production subscription, or Resource Policy Contributor plus the ability to create role assignments for a managed identity, at the mg-northfork scope; the management-group tree from the governance build; the six-tag schema decided (Environment, Owner, Contact, CostCenter, Workload, ManagedBy); and the Azure CLI. Set the identifiers the commands use: the management-group scope is /providers/Microsoft.Management/managementGroups/mg-northfork and the Production subscription is PROD_SUB_ID. The commands look built-in definitions up by display name so you never paste a wrong GUID.
Step 1: Assign the tag and location policies, not enforcing
Assign the enforcing policies to the top of the tree, but assign them in do-not-enforce mode so they evaluate and report without blocking anything yet. This is the whole audit-before-deny discipline made concrete: the policy is a real deny, but its block is withheld while you watch. Use the built-in that requires a tag on resource groups, one assignment per required tag, and the built-in that restricts resource locations. Assign them at the management-group scope so inheritance carries them to every subscription beneath.
| Setting | Value | Why |
|---|---|---|
| Policy | Require a tag on resource groups; Allowed locations; Allowed locations for resource groups | The built-ins that enforce tagging and region discipline, by their real names |
| Scope | mg-northfork | Assigned once at the top; inherited by every subscription below |
| Enforcement mode | DoNotEnforce | Evaluates and reports, but the Deny does not act; this is the safe trial |
| Locations value | westus2, eastus, and global for a custom policy | The built-in already excludes global; a custom location policy must add it or it blocks Entra and other global services |
# Look up the built-in definitions by name (avoids pasting GUIDs)
REQTAG=$(az policy definition list --query "[?displayName=='Require a tag on resource groups'].name" -o tsv)
ALLOWLOC=$(az policy definition list --query "[?displayName=='Allowed locations'].name" -o tsv)
ALLOWLOCRG=$(az policy definition list --query "[?displayName=='Allowed locations for resource groups'].name" -o tsv)
MG="/providers/Microsoft.Management/managementGroups/mg-northfork"
# Require CostCenter on resource groups, trialed non-enforcing (repeat per required tag)
az policy assignment create --name "req-tag-costcenter-rg" \
--scope "$MG" --policy "$REQTAG" \
--params '{"tagName":{"value":"CostCenter"}}' \
--enforcement-mode DoNotEnforce
# Restrict locations, also non-enforcing to start
az policy assignment create --name "allowed-locations" \
--scope "$MG" --policy "$ALLOWLOC" \
--params '{"listOfAllowedLocations":{"value":["westus2","eastus"]}}' \
--enforcement-mode DoNotEnforce
Expected result: the assignments exist and begin evaluating, but nothing is blocked. Failure mode to avoid: assigning these with enforcement left at its default, which turns them into live denies immediately and, on a brownfield subscription, can block a deployment before you have seen what it would catch. The mode is the safety valve; use it.
Step 2: Observe the flood and scan for what would block
Let the policies evaluate, then look at what they found. On an existing subscription you will see a compliance flood, many resources reported non-compliant at once, and that is the foundation surfacing drift, not breaking. Trigger an on-demand scan so you are not waiting on the evaluation cycle, then read the non-compliant results and decide, resource by resource, what to remediate and what to exempt. This is the evidence that makes the promotion in Step 3 a decision rather than a gamble.
# Trigger an on-demand compliance scan for the subscription
az policy state trigger-scan --subscription "PROD_SUB_ID"
# List what the non-enforcing policies would block if promoted
az policy state list --subscription "PROD_SUB_ID" \
--filter "complianceState eq 'NonCompliant'" \
--query "[].{policy:policyAssignmentName, resource:resourceId}" -o table
Expected result: a concrete list of exactly which resources violate each policy today. Failure mode: skipping this step and promoting to deny on faith, which is how a governance rollout blocks a legitimate deployment nobody realized was non-compliant. The scan is the difference between watching and guessing.
Step 3: Promote to deny, once it is clean
When the non-compliant list is either empty or entirely accounted for by exemptions you have decided to grant, flip the assignment from do-not-enforce to enforcing. From that moment the policy blocks new violations at creation. Promote one policy at a time, not all at once, so that if something unexpected breaks you know precisely which rule did it.
# Promote a single trialed policy to enforcing
az policy assignment update --name "req-tag-costcenter-rg" --scope "$MG" \
--enforcement-mode Default
# Confirm the mode flipped
az policy assignment show --name "req-tag-costcenter-rg" --scope "$MG" \
--query "enforcementMode" -o tsv
Expected result: the assignment reports Default enforcement and now denies non-compliant creations. Failure mode: promoting every policy in one pass, so that when a deployment starts failing you are left guessing which of a dozen freshly enforcing rules is responsible. One at a time is slower and it is correct.
Step 4: Inherit tags down with Modify, and remediate
Denying an untagged resource group stops the problem where a human creates it, but the resources inside the group do not inherit its tags on their own. The modify effect fixes that: it stamps each tag from the resource group onto the resources within it as they are created, and a remediation task reaches back to tag everything that already exists. Because modify changes resources, its assignment needs a managed identity with rights to write tags, which you grant after the assignment creates the identity.
| Setting | Value | Why |
|---|---|---|
| Policy | Inherit a tag from the resource group (Modify) | Stamps the resource-group tag onto resources; one assignment per tag |
| Identity | System-assigned managed identity | Modify changes resources, so the assignment must act as an identity |
| Identity role | Tag Contributor at mg-northfork | The least privilege that lets it write tags across the scope |
| Remediation | A remediation task after assignment | Modify only acts on new writes; remediation backfills existing resources |
INHERIT=$(az policy definition list --query "[?displayName=='Inherit a tag from the resource group'].name" -o tsv)
# Assign the Modify policy with a system-assigned identity (repeat per tag)
az policy assignment create --name "inherit-costcenter" \
--scope "$MG" --policy "$INHERIT" \
--params '{"tagName":{"value":"CostCenter"}}' \
--mi-system-assigned --location "westus2"
# Grant the assignment's identity the right to write tags
PID=$(az policy assignment show --name "inherit-costcenter" --scope "$MG" --query "identity.principalId" -o tsv)
az role assignment create --assignee-object-id "$PID" --assignee-principal-type ServicePrincipal \
--role "Tag Contributor" --scope "$MG"
# Backfill existing resources
az policy remediation create --name "inherit-costcenter-remediation" \
--policy-assignment "inherit-costcenter" --management-group "mg-northfork"
Expected result: new resources are stamped with the inherited tag on creation, and the remediation task tags the existing ones within a cycle. Failure mode to watch: assigning the modify policy and never granting its identity Tag Contributor, so it reports non-compliant and silently changes nothing, exactly the same trap the diagnostic-settings policy sets in the logging build. If it is not tagging, check the identity’s role first.
Step 5: Exempt what the platform forbids, and lock the crown jewels
Two things belong at the end because they are about the exceptions to enforcement rather than enforcement itself. The first is the exemption every hub network needs. If you require a network security group on every subnet, the gateway subnet will violate it, because the Azure platform does not permit an NSG on that subnet at all. The answer is a mitigated exemption at the narrowest scope, with the platform requirement recorded as the reason, never disabling the policy that still has to govern every other subnet.
# Exempt the GatewaySubnet from the subnet-NSG policy as a documented Mitigated exemption
az policy exemption create --name "gatewaysubnet-nsg-exempt" \
--policy-assignment "SUBNET_NSG_ASSIGNMENT_ID" \
--exemption-category Mitigated \
--scope "/subscriptions/PROD_SUB_ID/resourceGroups/rg-network-prod-wus2-001/providers/Microsoft.Network/virtualNetworks/vnet-hub-prod-wus2-001/subnets/GatewaySubnet" \
--expires-on "2027-07-01T00:00:00Z" \
--description "GatewaySubnet cannot carry an NSG (Azure platform requirement); compensating controls on the gateway."
The second is protecting the resources the whole estate depends on from being deleted. The direct, immediate control is a delete lock on the critical resource groups, the hub network, the management workspace, the key data stores, which stops an accidental or malicious deletion cold. At estate scale, the deny-action policy effect does the same thing as a governed rule rather than a per-resource lock, blocking deletion across everything in scope while the platform keeps you from ever locking yourself out. Start with the lock on the crown jewels; reach for the policy when you have enough of them to be worth governing centrally.
# Immediate protection: a delete lock on a critical resource group
az lock create --name "no-delete-hub" --lock-type CanNotDelete \
--resource-group "rg-network-prod-wus2-001" \
--notes "Hub networking: protected from deletion. Remove deliberately, with change control."
A worked example
Northfork assigned the require-tag and allowed-location policies non-enforcing, scanned, and found a handful of resource groups from the accidental-IaaS days missing CostCenter and one older resource group in an unapproved region. It tagged the groups it could, decided the odd-region group was a legitimate legacy exception, and granted it a waiver with a ninety-day removal trigger to migrate it. With the non-compliant list down to that one waived group, it promoted the tag and location policies to enforcing, one at a time. It assigned the inherit-a-tag modify policies, granted the identity Tag Contributor, and ran remediation, so the file-server virtual machine that had been created untagged now carried Owner, Contact, and CostCenter inherited from its resource group. It exempted the gateway subnet from the subnet-NSG requirement as a mitigated exemption, and placed a delete lock on the hub network resource group. The result is an estate where a new untagged resource group is now refused at creation, existing resources carry their inherited tags, the one legitimate exception is documented with an expiry, and the hub cannot be deleted by accident.
Naming convention
Name policy assignments for what they do and how they are enforced, so the assignment list reads as the estate’s rulebook: req-tag-costcenter-rg, allowed-locations, inherit-costcenter, and so on. Name exemptions for the object and the reason, gatewaysubnet-nsg-exempt, so a reviewer can tell at a glance what was carved out and why. Keep the exemption register, what is exempt, which category, who owns it, and when it is reviewed or expires, in the same document as the rest of the foundation, because an exemption nobody revisits is how a temporary waiver quietly becomes permanent. The assignment names are self-documenting; the exemptions are the part that needs a human keeping the list.
The end-to-end test
Prove the guardrail both blocks and permits, because a policy that only ever blocks might just be broken. Attempt to create a resource group without the required tag and confirm it is refused; then create one with the tag and confirm it succeeds. A rule that says no to the non-compliant case and yes to the compliant one is enforcement working exactly as designed, and it is the single test that tells you the tree is finally carrying rules rather than just describing an intention.
# Should FAIL once the tag policy is enforcing: a resource group with no CostCenter tag
az group create --name "rg-test-noncompliant" --location "westus2"
# Expect: a policy error naming the require-tag assignment (RequestDisallowedByPolicy)
# Should SUCCEED: the same resource group created with the required tag
az group create --name "rg-test-compliant" --location "westus2" \
--tags CostCenter=IT Owner=Platform [email protected] \
Environment=prod Workload=test ManagedBy=MSP
# Expect: created; a resource placed in it inherits these tags after remediation
Before you scale it
- Tag and location policies assigned at mg-northfork in DoNotEnforce mode, evaluated, and scanned before any of them was allowed to block.
- Compliance flood triaged: everything either remediated or granted a documented exemption with a category and, for waivers, a removal trigger.
- Policies promoted to enforcing one at a time, each confirmed at Default enforcement.
- Tag inheritance assigned with a system-assigned identity granted Tag Contributor, and a remediation task run to backfill existing resources.
- Gateway subnet exempted from the subnet-NSG requirement as a Mitigated exemption at the narrowest scope, with the platform reason recorded.
- Crown-jewel resource groups protected with a delete lock, with deny-action noted for estate-scale delete protection.
- End-to-end test passed: a non-compliant resource group is refused and a compliant one is accepted.
For an estate large enough that hand-assigning policies stops scaling, the same guardrails ship as a maintained set in the Azure Verified Modules based landing-zone accelerator, in Bicep or Terraform, which is the point to adopt it rather than at the beginning. With enforcement in place, the foundation is built. What remains is the discipline of running it and growing it without a rebuild, and the mandatory tags you just enforced are about to start paying for themselves. That is the closing article.
Azure Landing Zones
‹ Previous: [LZ 7] Where the Tree Starts Saying No
Next: [LZ 8] Grows by Placement, Not Rebuild ›




