CoursesAzure securityManagement groups & landing zones

Management groups & landing zones

Inherited guardrails, secure-by-default subscriptions.

Advanced30 min · lesson 14 of 15

When a new team moves onto a floor of a leased office tower, they do not install their own smoke detectors, rewire the fire exits, or bolt badge readers to the doors. The building already has all of that, wired in long before anyone showed up, and every floor a team occupies is protected the moment they get the keys. Azure gives you the same trick. The tower is your tenant (your whole Microsoft Entra ID directory, the identity system formerly called Azure Active Directory). Each floor is a subscription (a billing and resource boundary). The wiring, the part that makes safety a property of the building instead of a chore each team has to remember, is a management group hierarchy.

This lesson is about making security something a subscription is born with, not a hardening checklist someone runs after the fact, or forgets to. Get the hierarchy right and a subscription created next year inherits every guardrail you wrote this year, automatically, before a single workload lands in it.

The control plane above your subscriptions

A management group (MG for short) is a container that sits above subscriptions. Every tenant is born with one built-in Tenant Root Group at the very top. Beneath it you can nest management groups up to six levels deep (not counting the root itself or the subscriptions at the bottom), and a single tenant can hold up to 10,000 of them. Picture them as the floor plan of the tower: a way to group subscriptions so you can wire one set of controls to a whole wing at once.

The reason any of this matters for security is one word: inheritance. Attach an Azure Policy assignment or a role assignment to a management group and it flows downhill, to every subscription under it, every resource group inside those, and every resource inside those, including ones that do not exist yet. Scope is the technical name for the level an assignment attaches to. A child inherits everything its ancestors set, and can pile more on top, but it can never take away what a parent handed down. Assign your baseline once, high in the tree, and you stop re-hardening each subscription by hand, which is the single biggest source of drift once you have more than a handful of them.

The same lever moves in both directions. A careless Allow at the root grants that permission everywhere at once. And a policy that writes to resources, one that deploys or rewrites them rather than only watching, does the mirror image: set it at the root and it starts changing resources across the entire tenant. Scope is power, and the highest scopes carry the most risk.

The top of the tree is production
Inheritance flows one way, down, and a child cannot cancel what a parent set. So an Allow granted at the Tenant Root Group or an intermediate root lands in every subscription at once, and a policy that deploys or modifies resources there (not one that only audits) starts writing across the whole tenant. Guard the top two or three management groups with the same change control you put on a production deploy. Test new policy at a low, throwaway management group (a sandbox branch) before you ever raise its scope.

Build the hierarchy and place a subscription

The Cloud Adoption Framework (CAF, Microsoft's reference design for running Azure at scale) lays out a standard shape. Under the tenant root sits an intermediate root you own, then a platform branch (identity, management, and connectivity subscriptions that host shared services), a landing-zones branch for real workloads (usually split into corp for private internal apps and online for internet-facing ones), plus a lightly governed sandbox for experiments and a decommissioned branch where subscriptions wait to be deleted. Where you drop a subscription decides which guardrails it inherits, so placement is a security decision, not filing. A subscription has exactly one parent management group at any instant; adding it to corp atomically pulls it out of wherever it was, with no in-between state where it belongs to both or neither.

terminal
# The Tenant Root Group already exists; its NAME equals your tenant ID.
ROOT=$(az account show --query tenantId -o tsv)
# Build two tiers under the root: a platform branch and a landing-zones branch.
az account management-group create --name platform --display-name "Platform" --parent "$ROOT"
az account management-group create --name landing-zones --display-name "Landing Zones" --parent "$ROOT"
az account management-group create --name corp --display-name "Corp" --parent landing-zones
# Move an existing subscription under the Corp landing zone
# (idempotent: re-running changes nothing; about a minute to propagate).
az account management-group subscription add --name corp --subscription "Corp-Prod"
# Show the tree from landing-zones down (-e expands children, -r recurses every level below).
az account management-group show --name landing-zones -e -r \
--query "children[].{name:displayName, type:type}" -o table
output
Name Type
------ ----------------------------------------------
Corp Microsoft.Management/managementGroups

That reparenting takes about a minute to propagate. The moment it does, Corp-Prod is subject to everything assigned at landing-zones and corp above it, and you never touched the subscription itself to make that happen.

Assign the baseline once, inherit everywhere

One policy checks one thing. A real baseline has dozens of rules, and wiring them up one at a time is how gaps creep in. An initiative solves that the way a building's master safety code does: one binder that bundles every separate rule (sprinkler spacing, exit-sign wattage, alarm wiring) so an inspector applies the whole set in a single pass. An initiative (Azure's word for a policy set, many individual policy definitions gathered under one assignment) pushes a whole baseline in one move. The built-in Microsoft Cloud Security Benchmark (MCSB, Microsoft's default catalog of security controls, and the current name for what used to be the Azure Security Benchmark) is the natural starting point. A v2 is in preview; the built-in initiative you assign today is the generally available v1. It mixes two kinds of policy. Audit policies watch and flag drift without touching anything. DeployIfNotExists policies (DINE, the ones that create a missing control for you, like switching on diagnostic logging when a resource has none) actually change your resources.

Because DeployIfNotExists and Modify policies write to your resources, the assignment needs an identity to act as, the same way a maintenance crew carries its own badge for the doors it services instead of borrowing yours. That is what --mi-system-assigned gives it: a system-assigned managed identity (a service account Entra ID creates, holds, and rotates for you, so there is no password or key to leak). A --location is required because that identity is itself a small resource that has to live somewhere. The identity is powerless until you grant it a role, which is what --role Contributor scoped by --identity-scope does. Assign the initiative at the landing-zones management group, then prove a child subscription sees it as inherited rather than attached locally.

terminal
MG=/providers/Microsoft.Management/managementGroups/landing-zones
# Assign the Microsoft Cloud Security Benchmark initiative once, with a
# remediation identity so its DeployIfNotExists policies can act.
az policy assignment create \
--name mcsb-baseline \
--display-name "MCSB baseline" \
--policy-set-definition 1f3afdf9-d0c9-4c3d-847f-89da613e70a8 \
--scope "$MG" \
--location eastus \
--mi-system-assigned \
--identity-scope "$MG" \
--role Contributor \
--query "{name:name, idType:identity.type, principalId:identity.principalId}" -o yaml
output
idType: SystemAssigned
name: mcsb-baseline
principalId: 9b1e7c4a-3f80-4a2d-9c11-5e6f0d2a7b34

Now switch into the child subscription and confirm the assignment actually reaches down to it.

terminal
# Switch into a child subscription.
az account set --subscription "Corp-Prod"
# Without --disable-scope-strict-match this lists ONLY assignments attached
# at the current scope, and you would wrongly conclude nothing protects you.
az policy assignment list --disable-scope-strict-match \
--query "[?name=='mcsb-baseline'].{name:name, inheritedFrom:scope}" -o table
output
Name InheritedFrom
------------- --------------------------------------------------------------
mcsb-baseline /providers/Microsoft.Management/managementGroups/landing-zones

The InheritedFrom column pointing back at the management group is your proof. The subscription holds no copy of this assignment. A delegated subscription admin cannot edit it or delete it. They get to run workloads inside the fence, not move the fence.

Deny assignments, the guardrail that outranks an Owner

Azure RBAC (role-based access control, the system that decides who is allowed to do what) is additive. When several role assignments apply to the same person, the most permissive one wins. That has a sharp edge: a subscription Owner can grant themselves almost any permission, including ones you would rather they never hold. One mechanism beats an Allow, and only one. It works like a sealed-by-order notice taped across a door: your master key still exists, but that door will not open for it. A deny assignment is that notice, an explicit, targeted deny that outranks every role assignment, so even an Owner is stopped cold.

You cannot create one with az role assignment. Deny assignments are produced for you by two things: Azure managed applications, and Deployment Stacks configured with denySettings (the supported replacement for Azure Blueprints, which Microsoft has deprecated). A Deployment Stack with denySettings.mode set to denyDelete or denyWriteAndDelete is how you stop a delegated admin from deleting a landing zone's hub network or quietly undoing a lock you applied on their behalf. Before you trust any such boundary, read what is actually in force, because deny assignments never show up in az role assignment list. You have to ask Azure Resource Manager (ARM, the control-plane interface every az command ultimately calls) for them directly.

terminal
SUB=$(az account show --query id -o tsv)
# Deny assignments do NOT appear in 'az role assignment list'.
# Ask Azure Resource Manager for them directly.
az rest --method get \
--url "https://management.azure.com/subscriptions/$SUB/providers/Microsoft.Authorization/denyAssignments?api-version=2022-04-01" \
--query "value[].{name:properties.denyAssignmentName, denies:properties.permissions[0].actions[0], appliesTo:properties.principals[0].displayName, exempt:properties.excludePrincipals[0].type}" -o table
output
Name Denies AppliesTo Exempt
-------------------- -------------------------- --------- ----------------
DenyDeleteHubNetwork Microsoft.Network/*/delete Everyone ServicePrincipal
stack-corp-lz */write Everyone ServicePrincipal

Read that output like the security control it is. DenyDeleteHubNetwork blocks network deletes for Everyone, exempting only the service principal that manages it. The Deployment Stack's own deny setting blocks writes the same way. If either row ever disappears, someone changed the stack or removed the managed app, and that is a finding to chase, not a cleanup to celebrate.

Landing zones and subscription vending

Stack all of this together, the management-group tree, the baseline policies, the shared networking, and one central place to send logs, delivered as infrastructure as code (IaC, your whole environment written down in version-controlled files instead of console clicks), and you have what the Cloud Adoption Framework calls an Azure Landing Zone (ALZ). Subscription vending is the piece that makes it behave like a vending machine: a team asks for a subscription, and an automated pipeline hands one back that is already placed, already logging, already defended.

In practice that pipeline is one module you run. The Terraform lz-vending module (published as Azure/lz-vending/azurerm) is what most existing setups use. Microsoft has since archived that standalone module and folded the job into Azure Verified Modules (AVM, its catalog of supported, pre-reviewed building blocks), so new landing zones should track the AVM subscription-vending pattern module (there is a Bicep path too, if you deploy that way). Whichever one you run, the work is identical. In a single terraform apply it creates the subscription, wires its networking, and grants the opening role assignments. It does not hand-configure logging or Defender for Cloud (the current name for the product once called Azure Security Center) itself. It does not need to. The moment the subscription lands under corp, the inherited MCSB baseline, the diagnostic-settings DINE policy, and the Defender for Cloud plans all switch on by inheritance. The team gets a subscription that was fenced before they logged in, with no gap between 'subscription exists' and 'subscription is protected' for an attacker or an honest mistake to slip through.

main.tf
# Subscription vending with the Terraform lz-vending module.
# NOTE: as of late 2025 this standalone module is archived; Microsoft now ships
# subscription vending through Azure Verified Modules (AVM). Inputs and the
# inheritance payoff carry straight over to the AVM successor.
module "corp_payments_lz" {
source = "Azure/lz-vending/azurerm"
version = "~> 7.0"
location = "eastus"
# Create the subscription...
subscription_alias_enabled = true
subscription_display_name = "corp-payments-prod"
subscription_alias_name = "corp-payments-prod"
subscription_workload = "Production"
subscription_billing_scope = "/providers/Microsoft.Billing/billingAccounts/1234567/enrollmentAccounts/42"
# ...and file it under the Corp management group, where the MCSB baseline,
# the diagnostic-settings DINE policy, and the Defender plans all inherit down.
subscription_management_group_association_enabled = true
subscription_management_group_id = "corp"
}
How a vended subscription is fenced before anyone logs in
1Team requests a subscription
a pull request to the vending repo
2Module builds it
vending module: subscription, network, roles
3Filed under the right MG
corp, online, or sandbox
4Baseline inherits at once
MCSB, diagnostics DINE, Defender plans
5Handed over, already fenced
no window of exposure
Every step is automated; the team receives a subscription that is already governed.

Verify the fence actually holds

Assigned is not the same as enforced, and vended is not the same as verified. After you move or vend a subscription, check that the controls are live, not merely declared. Two checks catch most of what goes wrong. First, confirm the Defender for Cloud plans you require report Standard and not Free. Second, ask the management group how many resources under it are non-compliant right now.

terminal
# 1) Are the Defender for Cloud plans we require actually on (Standard, not Free)?
az security pricing show -n VirtualMachines --query "{plan:name, tier:pricingTier}" -o table
az security pricing show -n CloudPosture --query "{plan:name, tier:pricingTier}" -o table
# 2) How many resources under the management group are non-compliant right now?
az policy state summarize --management-group landing-zones \
--query "value[0].results.nonCompliantResources"
output
Plan Tier
--------------- --------
VirtualMachines Standard
Plan Tier
------------ --------
CloudPosture Standard
7

A non-zero non-compliant count on a subscription you believe is fully fenced is a finding, not background noise. Chase it down. And keep one eye on the bill while you do: assigning Azure Policy costs nothing, but Defender plans charge per resource or per hour, so turning on every plan across every subscription through an inherited policy is a real spending decision. Make it on purpose, not by accident.

Inheritance never fixes what already exists
Placing a subscription under a management group applies the baseline going forward only. DeployIfNotExists and Modify policies fire when a resource is created or updated, never retroactively. Every VM, storage account, and subnet already sitting in that subscription stays non-compliant, with no diagnostics flowing, until you run a remediation task by hand. Teams see a green 'compliant assignment' status, assume they are covered, and stay half-dark for weeks. After moving a live subscription, always remediate, then confirm it finished.

One catch on remediation. Because MCSB is an initiative holding many policies, a remediation task has to name the single policy you are fixing, by its reference id (the internal handle the initiative gives each policy it wraps). Read the reference ids out of the set definition, pick the one you want, then start the task. --resource-discovery-mode ReEvaluateCompliance tells Azure to re-scan compliance before it acts, which matters right after a move when the old compliance data is stale.

terminal
# DINE fixes fire only on resource create/update; to fix resources that
# ALREADY exist, run a remediation task by hand.
# MCSB is an initiative, so the task must target ONE policy by its reference id.
# Read the reference ids the initiative exposes:
az policy set-definition show --name 1f3afdf9-d0c9-4c3d-847f-89da613e70a8 \
--query "policyDefinitions[].policyDefinitionReferenceId" -o tsv | head -3
# Remediate the diagnostic-settings policy across resources already in place.
az policy remediation create \
--name fix-vm-diagnostics \
--management-group landing-zones \
--policy-assignment mcsb-baseline \
--definition-reference-id vmDiagnosticSettings \
--resource-discovery-mode ReEvaluateCompliance \
--query "{name:name, state:provisioningState, discovery:resourceDiscoveryMode}" -o yaml
output
vmDiagnosticSettings
storageAccountDiagnostics
keyVaultDiagnostics
discovery: ReEvaluateCompliance
name: fix-vm-diagnostics
state: Accepted
Quick check
01You assign the MCSB initiative at the landing-zones management group. Six months later, a brand-new subscription is created and placed under landing-zones. What protection does that new subscription have?
Correct — inheritance is continuous, so any subscription later placed under the MG picks up the assignment instantly.
Incorrect — re-scoping to each new subscription is exactly the manual drift the hierarchy exists to remove.
Incorrect — an assignment is not a point-in-time snapshot; it applies to whatever sits under its scope now, future children included.
Incorrect — an assignment has a single scope, not a list, and children inherit with no per-subscription edit.
02A subscription Owner tries to delete the hub virtual network. The delete fails, even though the Owner role includes Microsoft.Network/*/delete. What is the most likely reason?
Incorrect — Azure RBAC is additive, so a lesser role can never cancel a permission Owner already grants.
Correct — deny assignments are the one mechanism that beats an Allow, which is why they stop even an Owner.
Incorrect — Owner grants *, which already covers network deletes.
Incorrect — policy does not rewrite built-in role definitions; the block comes from a deny assignment.
03You move a live subscription with 200 existing VMs under landing-zones. An hour later, az policy state summarize still reports most of them non-compliant with no logs flowing, even though MCSB includes a DeployIfNotExists policy for diagnostic settings. You run az policy remediation create --policy-assignment mcsb-baseline and it errors out. What is happening, and what fixes it?
Incorrect — MG inheritance propagates in about a minute, and DINE never retroactively fixes existing resources on its own.
Incorrect — a subscription only ever has one parent MG, and the move is atomic, so there is no two-parent state.
Correct — DINE is not retroactive, and remediating one policy inside an initiative requires that policy's reference id.
Incorrect — an MG-scoped diagnostic-settings assignment is valid and is the standard way to deploy it.

Guardrails lower how often you get breached and shrink the damage when you do. They do not make incidents vanish. When one lands, the same hierarchy that pushed policy downhill is what guarantees the evidence is already there: Activity Logs, Defender alerts, and diagnostic streams flowing into central, tamper-resistant storage from minute zero, instead of something a responder has to switch on mid-crisis. Where that evidence lives, and how you turn it into a timeline you can defend, is where the next lesson on incident response and forensics picks up.

Try this

Work through “Verify the fence actually holds” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

The trap worth remembering here: the top of the tree is production. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related