CoursesAzure Administrator AssociateGovernance: Policy, locks & tags

Governance: Policy, locks & tags

Management groups, Azure Policy, locks, cost.

Intermediate30 min · lesson 3 of 15

Role-based access control (RBAC), the subject of the last lesson, is the key cabinet for your Azure estate. It decides who gets into which building. Governance is the rest of what a working city needs. Zoning law that says what may be built and where, which in Azure is Azure Policy. Heritage protection that stops anyone, the owner included, from knocking down a landmark, which is a resource lock. A land registry that records which department owns each plot and who pays the rates on it, which is what tags do. These are guardrails rather than polite suggestions, and the reason is *where* they run. Every create, update and delete goes through one front door called Azure Resource Manager (ARM), the single application programming interface (API) sitting in front of every Azure service. The portal talks to it. So does the az command-line interface (CLI), so does Terraform, so does any raw REST call you write yourself. Governance is enforced right at that door, before the request ever reaches the service that would have carried it out. Nobody has to remember the rules, because the platform will not break them.

Where governance attaches: the scope tree

Governance attaches to *scopes*, and scopes are arranged like a family tree. At the top of every Microsoft Entra ID tenant (the directory holding your organization's identities; it used to be called Azure AD) sits a single tenant root management group. Underneath it you can nest management groups, folders whose only job is to gather subscriptions together for governance. You can go *six levels deep*, and the root and the subscriptions themselves do not count toward that limit, a detail AZ-104 enjoys asking about. Each subscription hangs off exactly one management group. Subscriptions hold resource groups, and resource groups hold resources. Policy and RBAC assignments *flow downhill on their own*. Assign a policy to a management group and all forty subscriptions beneath it are covered in one move, and a subscription you drag in next month picks up every assignment the moment it lands. That is the difference between governing forty subscriptions and governing one tree. Most organizations keep the hierarchy small: a *platform* group at the top, then *prod*, *non-prod* and *sandbox* under it, with the rules getting stricter the closer you get to production.

management-group-tree
# Build a hierarchy under the tenant root (max 6 levels of nesting)
az account management-group create --name platform --display-name "Platform"
az account management-group create --name platform-prod \
--display-name "Production" --parent platform
# A subscription lives under exactly ONE management group — move it in
az account management-group subscription add \
--name platform-prod --subscription "Contoso-Prod-01"
# Inspect the tree
az account management-group show --name platform --expand --recurse \
--query "{mg:displayName, children:children[].{mg:displayName, subs:children[].displayName}}"
# {
# "mg": "Platform",
# "children": [
# { "mg": "Production", "subs": ["Contoso-Prod-01"] }
# ]
# }

Azure Policy: rules the platform keeps for you

A policy definition is an if/then rule written in JSON (JavaScript Object Notation, the text format Azure uses to describe every resource): *if* a resource matches these conditions, *then* apply this effect. Learn the effects cold. deny rejects the request outright, so it is preventive. audit lets the request through but marks the resource non-compliant, so it is detective. auditIfNotExists flags a resource when something *related* to it is missing, a diagnostic setting for example. deployIfNotExists goes and creates that missing related thing for you. modify adds or corrects properties, tags most often, while the request is still in flight. append bolts extra fields onto the request. A definition on its own does nothing at all. An assignment is what binds it to a scope, with parameters and any exclusions you need, and a definition can only be assigned at or *below* the scope where it is stored. Keep one split sharp in your head for the exam: RBAC decides who may act, Policy decides what a resource is allowed to look like. A deny policy stops an Owner every bit as firmly as it stops a Reader.

The mechanics are worth knowing. When ARM receives a PUT or PATCH (the two web requests behind creating and updating a resource), it checks every assignment whose scope covers the target *before* handing the request to the resource provider, the service such as Microsoft.Storage or Microsoft.Compute that actually builds the thing. The field paths a policy tests are called aliases, and they map straight into that request body. Two timing facts catch people out. A brand new assignment takes roughly *five minutes* before it starts applying at its scope. Resources that already exist are only rechecked by a compliance scan about every 24 hours. So deny stops new violations and never touches old ones. The old ones sit there flagged as non-compliant until you go and deal with them yourself.

deny-public-blob
# The rule: an if/then over the resource's ARM JSON
cat > deny-public-blob.rules.json <<'EOF'
{
"if": {
"allOf": [
{ "field": "type", "equals": "Microsoft.Storage/storageAccounts" },
{ "field": "Microsoft.Storage/storageAccounts/allowBlobPublicAccess",
"equals": "true" }
]
},
"then": { "effect": "deny" }
}
EOF
# A definition is assignable only at or below where it lives — to assign
# at a management group, store it ON that management group:
az policy definition create --name deny-public-blob --mode Indexed \
--management-group platform-prod \
--display-name "Deny storage accounts with public blob access" \
--rules deny-public-blob.rules.json
az policy assignment create --name deny-public-blob \
--policy "/providers/Microsoft.Management/managementGroups/platform-prod/providers/Microsoft.Authorization/policyDefinitions/deny-public-blob" \
--scope "/providers/Microsoft.Management/managementGroups/platform-prod"
# ~5 minutes later, try to violate it:
az storage account create --name stpublicoops --resource-group rg-app \
--sku Standard_LRS --allow-blob-public-access true
# (RequestDisallowedByPolicy) Resource 'stpublicoops' was disallowed by policy.
# Policy identifiers: '[{"policyAssignment":{"name":"deny-public-blob"},
# "policyDefinition":{"name":"deny-public-blob"}}]'
# Code: RequestDisallowedByPolicy
Three governance guardrails, all enforced at ARM
Azure Policy: governs configuration
deny
Rejects non-compliant writes; preventive, and blocks an Owner as firmly as a Reader
audit
Allows the write but flags it non-compliant; detective only
deployIfNotExists / modify
The only effects that fix EXISTING resources, via a remediation task run by the assignment's managed identity
Locks: govern existence
CanNotDelete
Edits allowed, deletion blocked; the safe default
ReadOnly
Blocks every non-GET call, including POSTs like key listing and VM start
Tags: govern metadata
key:value labels
env, costCenter, owner; up to 50 per resource, and they carry no behaviour of their own
NOT inherited
RG tags don't flow to resources inside; use a modify policy plus remediation to backfill
Every create, update and delete funnels through Azure Resource Manager, where Policy and locks are enforced before the resource provider ever runs. Each guardrail binds everyone, Owner included. Policy inherits down the scope tree; locks cannot sit on management groups.

Initiatives, compliance and fixing what already exists

Assigning policies one at a time falls apart quickly, so Azure lets you bundle them into initiatives, which the documentation also calls *policy set definitions*. An initiative is a checklist rather than a single rule. The flagship built-in one is the Microsoft cloud security benchmark, previously named the Azure Security Benchmark, so be ready for either name on the exam. It carries 200+ member policies covering networking, encryption and logging. Assign it once at a management group and the whole branch below inherits that baseline. Two habits worth stealing from production teams. Roll out with --enforcement-mode DoNotEnforce first, so you can count the denials that *would* have happened without breaking anyone's Friday. And carve out the exceptions you genuinely need using exemptions, instead of deleting the assignment and losing the whole baseline. The remediation detail matters most: deployIfNotExists and modify are the *only* effects that touch resources which already exist, and they never do it behind your back. You create a remediation task, and the assignment's managed identity (an identity Azure creates and maintains for the assignment itself, which you then grant RBAC roles to) does the fixing. That is why the assignment below needs --mi-system-assigned. "Policy fixes it automatically, no identity required" is a stock wrong answer.

initiative-and-remediation
# Built-in initiatives are referenced by name (a GUID) — look it up:
MCSB=$(az policy set-definition list \
--query "[?displayName=='Microsoft cloud security benchmark'].name" -o tsv)
echo $MCSB
# 1f3afdf9-d0c9-4c3d-847f-89da613e70a8
# DeployIfNotExists/Modify members need an identity to fix things with:
az policy assignment create --name mcsb --policy-set-definition $MCSB \
--scope "/providers/Microsoft.Management/managementGroups/platform" \
--mi-system-assigned --location eastus
# Compliance rolls in once the evaluation cycle finishes (big scopes take a while):
az policy state summarize \
--query "results.{nonCompliantResources:nonCompliantResources, nonCompliantPolicies:nonCompliantPolicies}"
# {
# "nonCompliantResources": 118,
# "nonCompliantPolicies": 27
# }
# Fix EXISTING resources with a remediation task, run by that identity.
# Find reference ids inside the initiative first:
az policy set-definition show --name $MCSB \
--query "policyDefinitions[].policyDefinitionReferenceId" -o tsv | head -3
az policy remediation create --name fix-diag \
--management-group platform \
--policy-assignment "/providers/Microsoft.Management/managementGroups/platform/providers/Microsoft.Authorization/policyAssignments/mcsb" \
--definition-reference-id <reference-id-from-the-initiative>

Locks: the last line against fat fingers

Policy governs *configuration*. Locks govern *existence*. A lock is a sticker you put on a subscription, a resource group or a single resource, and it refuses destructive operations no matter who is asking. Locks cannot sit on management groups, which is the one placement rule to memorize. There are two kinds. CanNotDelete allows edits but refuses deletion. ReadOnly refuses any change at all. Locks inherit downward, and where two of them overlap the more restrictive one wins. Now the property that makes them useful: a lock binds everyone, Owner included. Getting past one means deliberately removing it first, which needs the Microsoft.Authorization/locks/* actions that, among the built-in roles, only Owner and User Access Administrator hold. That two-step is the entire point. It turns a 2 a.m. az group delete typo into a harmless error message. One boundary to internalize for the exam: locks work on the control plane only, meaning management operations that travel through ARM. A CanNotDelete lock on a storage account protects the *account*. It does nothing to stop someone emptying every blob *inside* it, because that traffic goes through the data plane, a separate door the lock never sees.

locks-in-action
# CanNotDelete: edits allowed, deletion blocked. ReadOnly: frozen solid.
az lock create --name protect-prod --lock-type CanNotDelete \
--resource-group rg-prod
# Even as Owner, this now fails:
az group delete --name rg-prod --yes
# (ScopeLocked) The scope '/subscriptions/.../resourceGroups/rg-prod' cannot
# perform delete operation because following scope(s) are locked:
# '/subscriptions/.../resourceGroups/rg-prod/providers/
# Microsoft.Authorization/locks/protect-prod'.
# Please remove the lock and try again.
# Removing a lock is a separate, auditable, deliberate act:
az lock delete --name protect-prod --resource-group rg-prod
A ReadOnly lock breaks more than editing
A ReadOnly lock refuses every control-plane operation that is not a GET, and that includes POST. Storage account keys come back from a POST call, so a ReadOnly lock on a storage account kills az storage account keys list along with every application authenticating using those keys. Put one on a resource group and you can no longer start or restart the VMs inside it, because those are POSTs too. Make CanNotDelete your default. Save ReadOnly for resources you truly want frozen solid, and find out what falls over in a lab before you roll it anywhere near production.

Tags: the metadata that money follows

Tags are sticky labels in key:value form (env=prod, costCenter=CC-1042, owner=platform-team) that you stamp on resources, resource groups and subscriptions. You get up to 50 per resource, names up to 512 characters, values up to 256. A tag does nothing on its own. Everything useful hangs off it: cost reports group by tag (the *Cost management* lesson builds directly on this), automation picks its targets by tag, and policies can require a tag or apply one for you. Here is the fact people miss most often: tags are not inherited. Tagging a resource group does *not* tag the resources sitting inside it, so a cost report grouped by a resource-group tag quietly reports every one of them as untagged, and finance ends up with a large unallocated pile. Two ways to fix it. Stamp the tags explicitly at creation time, or assign the built-in *Inherit a tag from the resource group* policy, a modify effect that injects the tag on every write, plus a remediation task to backfill everything you already have. Pair that with a deny-based *require a tag* policy on resource groups and nothing untagged is ever born.

tags-and-cost-allocation
# Tag at creation — key:value, max 50 per resource
az group create --name rg-app --location westeurope \
--tags env=prod costCenter=CC-1042 owner=platform-team
# RG tags do NOT flow down. Merge onto an existing resource explicitly:
az tag update --operation Merge --tags env=prod costCenter=CC-1042 \
--resource-id "/subscriptions/<sub-id>/resourceGroups/rg-app/providers/Microsoft.Storage/storageAccounts/stcontosoapp"
# The payoff — slice the whole estate by tag:
az resource list --tag costCenter=CC-1042 \
--query "[].{name:name, type:type}" -o table
# Name Type
# ------------ ---------------------------------
# stcontosoapp Microsoft.Storage/storageAccounts
# app-contoso Microsoft.Web/sites

Build this in order: hierarchy first, policy second, then locks and tags on anything that survives to production. Do that and every resource you create from here on is *born inside guardrails*, denied if it is dangerous, tagged so finance can find its owner, locked so nobody vaporizes it by accident. Which makes this the right moment to start creating real infrastructure. Next lesson: Virtual Machines & availability, your first az vm create, sizes and disks, and how availability sets and availability zones keep a workload running when hardware dies underneath it. Your new deny policies and required tags will be watching every VM you deploy, which is precisely how it should feel.

Left to itself, a subscription becomes a junk drawer inside a year. Each of the three controls answers a different question. Azure Policy checks every create and update against rules you choose, with audit, deny, append or deployIfNotExists deciding how loudly it reacts. Locks are blunter: CanNotDelete and ReadOnly sit on one resource or group and refuse the control-plane call even when RBAC would have allowed it. Tags block nothing whatsoever. They label things so Cost Management and your automation can work out who owns what.

Management groups sit above subscriptions, so putting Policy and RBAC assignments there means every new subscription arrives with the guardrails already on. That is the landing-zone idea in miniature: subscriptions that are secure by default, rather than hoping each team remembers to lock production themselves. For the exam, practice matching the control to the failure it prevents. "Nobody should be able to create public IP addresses" is Policy. "Do not delete this key vault" is a lock. "Who pays for this?" is a tag.

Try this

Build a small lab and watch each control fire on its own. Create a resource group, tag it, lock it, then add a deny policy that blocks public IP address creation. Test them one at a time so you can tell which control is the one stopping you.

terminal
RG=rg-lab-gov
az group create -n $RG -l eastus --tags env=lab owner=you
az group lock create -g $RG -n CannotDelete --lock-type CanNotDelete
# Inspect tags + lock
az group show -n $RG --query "{tags:tags,id:id}" -o json
az group lock list -g $RG -o table
output
$ az group show -n rg-lab-gov --query tags -o json
{
"env": "lab",
"owner": "you"
}
# Sample output
$ az group lock list -g rg-lab-gov -o table
Name LockType
------------- ------------
CannotDelete CanNotDelete

Takeaway

One line to carry away: Policy sets the rules across the whole estate, a lock stops an accidental delete or edit on a single scope, and a tag makes cost and ownership visible. Management groups are what let all three inherit cleanly, which is why you build the hierarchy before you build anything else.

Next time you have a lab subscription in front of you, put a deny effect policy at a management group, then push a matching Bicep deployment or portal change and watch it fail with the policy name printed in the error. Seeing your own rule reject your own deployment is what makes it stick.

Quick check
01You put a ReadOnly lock on a production storage account, expecting it to freeze the configuration and nothing else. Which operation breaks that you did not see coming?
Correct — Keys come back from a POST call, and a ReadOnly lock refuses every control-plane operation that is not a GET, so key retrieval breaks and the applications relying on it break with it.
Incorrect — No. Locks work on the control plane only. Deleting a blob is a data-plane operation the lock never sees.
Incorrect — No. ReadOnly allows GETs by design, so plain reads keep working.
Incorrect — No, and this is the misconception itself. ReadOnly refuses every non-GET call, POSTs like key listing and VM start or restart included.
02A compliance scan flags 118 resources that already exist as non-compliant with a policy. Which Azure Policy effect can actually go and fix resources that are already there, rather than only affecting new writes?
Incorrect — No. deny only rejects new non-compliant writes. It never deletes or changes something that already exists.
Incorrect — No. audit is detective only. It marks the resource non-compliant and changes nothing about it.
Correct — deployIfNotExists and modify are the only two effects that fix existing resources, and they do it through a remediation task performed by the assignment's managed identity.
Incorrect — No. append only bolts fields onto incoming write requests. It does nothing to existing resources during a scan.
03A cloud team runs 40 subscriptions under one management group. Nobody, not even a subscription Owner, may create a storage account with public blob access, and any subscription added to the group later has to be covered automatically. Which control fits best?
Correct — A deny policy at the management group blocks the write for everyone, Owners included, and inheritance covers the subscriptions you have today plus the ones you add tomorrow.
Incorrect — No. Locks govern deletion and existence, not configuration, so they cannot stop a badly configured account from being created in the first place.
Incorrect — No. RBAC governs who may act, not what a resource may look like. Stripping Owner would break legitimate management work and still would not stop the misconfiguration.
Incorrect — No. It works today, but a subscription added next month inherits nothing, and you now have 40 assignments to maintain instead of one.

Related