Skip to content
CAMPUX
Field notes · Governance
Azure Policy

Azure Policy, end to end: effects, remediation, and the five guardrails worth assigning

By 18 min read

This used to be nine short notes. It is one now, because the questions people actually bring to Azure Policy are one question with several parts: what will this rule do, in what order, to which resources, and how do I fix the ones that already exist? Here is the whole answer, with the built-in names and the commands checked against Microsoft's own definitions.

New to cloud? CAMPUX is a free, build-first course. Start here →

Azure Policy is the service that turns a governance rule into something the platform enforces. You assign a definition — an if that matches resources and a then that names an effect — to a management group, subscription or resource group. New resources are evaluated on the way in and can be denied, altered or logged; existing resources are reported daily and fixed through a remediation task that runs under a managed identity. The part the quick guides skip: the order effects run in, why remediation is a separate step, and the difference between an exclusion and an exemption are where real estates go wrong.

Two things to hold in mind before the detail. Azure Policy governs resources. Who may sign in, and under what conditions, is Microsoft Entra Conditional Access, and asking Policy to enforce MFA is the most common governance mistake there is. And the built-in library is large: for regions, tags, public exposure and diagnostics you assign a built-in and move on. You write a definition when the rule is yours.

What a policy is: definition, assignment, scope

A definition is JSON with three moving parts. A mode: Indexed evaluates only resource types that support tags and location, which is what the tag and region built-ins use; All evaluates everything including resource groups and subscriptions. Optional parameters, which make one definition reusable; the single best habit is to parameterise the effect itself, with Audit as the default. And the policyRule: an if built from fields, operators and allOf/anyOf, and a then naming exactly one effect.

A definition does nothing until it is assigned to a scope. Assignments inherit downward: assign at a management group and every subscription under it, including ones created next year, is covered. Assign as high as the rule is universally true and no higher. Several definitions can be bundled into an initiative (a policy set) and assigned as one unit with shared parameters, which is how a landing zone ships its whole baseline in one assignment.

Exclusion or exemption? They are not the same thing

An assignment can carry excluded scopes (notScopes): those resources are simply not evaluated and do not appear in the report. An exemption is a separate object created on a resource or scope that keeps it in scope but marks it Exempt, with a category — Waiver when the non-compliance is temporarily accepted, Mitigated when the intent is met another way — optional metadata such as who approved it and the ticket, and an optional expiresOn. Exempt resources still count toward overall compliance, the object survives its expiry for the record, and creating one needs the exempt/Action permission on the assignment as well as write on the scope. Exemptions are the auditable choice; exclusions are how things quietly disappear from the report.

The effects, and the order they run in

Every definition has one effect, and Microsoft documents eleven: addToNetworkGroup, append, audit, auditIfNotExists, deny, denyAction, deployIfNotExists, disabled, manual, modify and mutate. Five of them are the working set:

EffectWhen the rule matchesReach for it whenNeeds an identity?
denyBlocks the create or updateThe rule must never be brokenNo
auditAllows it, marks the resource non-compliantYou want to see reality before enforcingNo
modifyAdds, replaces or removes properties, most often tagsYou would rather fix than rejectYes
deployIfNotExistsChecks for a related resource and deploys it if missingA setting should exist automatically, such as a diagnostic settingYes
auditIfNotExistsFlags the missing related resource, changes nothingYou want the gap reported, not filledNo

The order matters, and Microsoft is exact about it. For a create or update request, disabled is checked first; then append and modify, because they can change the request and a change might stop a later audit or deny from firing; then deny, evaluated before audit so a blocked resource is not also logged twice; then audit, manual, auditIfNotExists, and denyAction last. After the resource provider returns success, auditIfNotExists and deployIfNotExists run to decide whether more logging or a deployment is needed. That last clause is why a deployIfNotExists policy cannot stop a resource being created: it acts after the resource exists.

Azure Policy evaluates a change and applies an effect: deny, audit, modify, or deploy-if-not-exists.resourcecreate / updateAzure Policyevaluates itDeny — blocks itAudit — logs itModify — changes itDeployIfNotExists
Figure — When you create or change a resource, Azure Policy evaluates it and applies an effect. Deny blocks the non-compliant change outright; Audit lets it through but flags it; Modify or Append quietly fixes the resource (adding a tag, say); DeployIfNotExists provisions whatever was missing. One engine, four levels of firmness.

When several assignments hit one resource, each is evaluated independently and the result is cumulative most restrictive: if any applicable assignment denies, the resource is blocked, whatever the others say. Overlapping policies tighten; they never loosen. If something you need is being blocked, the fix is in the assignments' scopes and exclusions, not in a looser policy.

Assign in audit, read the report, then deny

Never assign a new deny straight into production. Because the effect is a parameter, the discipline is a configuration change rather than a code change: assign as Audit, wait for the evaluation, read the compliance report, fix or exempt the surprises, then update the same assignment to Deny.

Know the timings, because they are the source of most "the policy is not working" tickets. A new or updated assignment takes about five minutes to apply to its scope, and then an evaluation cycle begins, with no fixed promise of when it finishes on a large scope. A resource created or updated inside an assigned scope has its result available roughly fifteen minutes later. Every assignment is re-evaluated once every twenty-four hours. If you cannot wait, start a scan yourself:

# evaluate a resource group now instead of waiting for the daily cycle
az policy state trigger-scan --resource-group rg-app-prod

# what is non-compliant, and against which assignment
az policy state list --filter "ComplianceState eq 'NonCompliant'" --query "[].{res:resourceId, assignment:policyAssignmentName}" -o table

Remediation: fixing the resources you already have

Assigning a modify or deployIfNotExists policy governs the future. It does not sweep through the four hundred storage accounts that already exist; they show up red in the report and nothing changes on its own. A remediation task is the deliberate act that runs the policy's deploy template or modify operations across the existing non-compliant resources. In the portal you can create the task at the same time as the assignment, for subscription-scoped assignments; for management-group assignments you create it after the first evaluation has found the resources.

A remediation task applies a deployIfNotExists/modify policy to existing resources, making them compliant.existing resourcesno diagnosticunencryptedmissing tagRemediationdeployIfNotExistsnow compliantdiagnostics onencryptedtag appliedpolicy guards new changes — a remediation task fixes what already exists
Figure — Assigning a policy only governs future changes; resources that already exist and violate it stay non-compliant until you act. A remediation task is that action: for deployIfNotExists or modify policies, Azure runs the effect across the existing estate — deploying the missing diagnostic setting, enabling encryption, applying the required tag — through a managed identity you grant the needed role. Assign the policy, then remediate to bring the back-catalogue into line.

The engine underneath is a managed identity on the assignment, either system-assigned or one you bring. Microsoft's wording: when Azure Policy deploys or modifies during remediation, it does so using the managed identity associated with the assignment, and that identity must hold the roles the definition lists in roleDefinitionIds. Built-ins carry that list already; a custom definition must declare it. Assign through the portal and the roles are granted for you. Assign through the CLI, PowerShell or an SDK and you grant them yourself, or the remediation fails on permissions. The identity is used only to make the change; evaluation itself runs as the caller.

# assign a modify/deployIfNotExists policy with a system-assigned identity
az policy assignment create \
  --name inherit-costcenter \
  --policy "ea3f2387-9b95-492a-a190-fcdc54f7b070" \
  --scope "/subscriptions/<sub>" \
  --params '{ "tagName": { "value": "CostCenter" } }' \
  --mi-system-assigned --location westeurope

# grant the role the definition asks for (Tag Contributor here), then remediate
az policy assignment identity assign --system-assigned \
  --name inherit-costcenter --identity-scope "/subscriptions/<sub>" \
  --role "Tag Contributor"

az policy remediation create --name backfill-costcenter \
  --policy-assignment inherit-costcenter

A task remediates one policy at a time; for an initiative you create one per policyDefinitionReferenceId. The defaults are ten resources in parallel, five hundred per task, and a failure threshold of one hundred percent, all adjustable. Progress and per-resource errors are on the task's page, and resources a task deployed are listed under the assignment.

Guardrail one: allowed locations

Region sprawl is quiet until it is a data-residency finding. The built-in Allowed locations (e56962a6-4747-49cd-b67b-bf8b01975c4c) is Indexed, takes a listOfAllowedLocations parameter, and its rule already ignores resources whose location is global and the Microsoft.AzureActiveDirectory/b2cDirectories type, so DNS zones and Front Door profiles do not trip it. A companion, Allowed locations for resource groups, governs the group's own location.

az policy assignment create \
  --name allowed-locations \
  --policy "e56962a6-4747-49cd-b67b-bf8b01975c4c" \
  --scope "/providers/Microsoft.Management/managementGroups/<mg>" \
  --params '{ "listOfAllowedLocations": { "value": ["westeurope", "northeurope"] } }'

Assign it at the top management group if the whole organisation must stay in two regions; new subscriptions are the ones people forget. Audit first: the surprises are the legitimately different region someone had a reason for, and those get an exemption with a ticket number, not a looser list.

Guardrail two: required and inherited tags

Tags are how an estate names itself: which team pays, who to call, whether this is production. They flow straight into Cost Analysis, so a bill that says "$40,000" becomes a bill that says which team spent it. And the fact that breaks every naive tagging plan is Microsoft's own sentence: resources don't inherit the tags you apply to a resource group or a subscription. Tag the group team = payments and the VMs inside it are still untagged. Discipline does not fix that; policy does.

Consistent tags let you group the bill by environment, owner, or cost-centre and enforce the scheme with policy.tag every resourceVMStorageSQL DBenv:prodowner:webcc:4471Cost, grouped by tagenv : prod$2,140env : dev$ 380owner : web$1,290cc : 4471$ 610one tag scheme → slice the bill, automate, and enforce with policy
Figure — Tags are key–value labels you attach to resources — env, owner, cost-center — and they are what make a large estate legible. Once everything is tagged consistently, Cost Management can group the bill by any tag (spend per environment, per team, per cost centre), automation can target resources by tag, and Azure Policy can require or inherit tags so the scheme does not rot. Agree the handful of tags up front; enforce them from day one.

Keep the required set small — a cost centre, an owner, an environment — because every required tag is friction at deploy time. Then use two kinds of built-in together:

Built-inEffectWhat it doesUse for
Require a tag on resources
871b6d14-10aa-478d-b590-94f262ecfa99
denyBlocks any resource created without the tagTags that are genuinely per resource, such as an owner
Inherit a tag from the resource group if missing
ea3f2387-9b95-492a-a190-fcdc54f7b070
modifyCopies the group's value onto the resource if it has none; remediation covers existing resourcesTags set once per group, such as cost centre and environment
Inherit a tag from the resource group
cd3aa116-8754-49c9-a813-ad46512ece54
modifyAdds or replaces the value from the groupWhen the group's value must win, even over a hand-typed one

Lead with inherit, because it never fails a deployment, remediate the existing estate, and only then add deny for the resource-specific tags. The modify assignments need the identity and a role that can write tags — Tag Contributor is enough. Three rules keep the scheme readable: tag names are case-insensitive but values are case-sensitive, so Prod and prod are two buckets on the report; never put a secret in a tag, because tags are stored as plain text and surface in cost exports, deployment history and logs; and the limit is fifty tags per resource, which you should never be near.

Guardrail three: no public IPs, public access off

Most incident write-ups contain the sentence "it was reachable from the internet and should not have been." Two moves make that sentence hard to write by accident. Deny public IPs on network interfaces, so a VM cannot be handed an internet address, and route legitimate ingress through a load balancer, Application Gateway, Front Door or Azure Firewall instead. Then turn public network access off on the PaaS services that ship reachable by default, so their only way in is a private endpoint.

Built-inEffectsCloses
Network interfaces should not have public IPs
83a86a26-fd1f-447c-b59d-e51f44264114
Audit, Deny, DisabledA public IP attached to any NIC
Storage accounts should disable public network access
b2982f36-99f2-4db5-8eff-283140c09693
Audit, Deny, DisabledStorage reachable over its public endpoint
Public network access on Azure SQL Database should be disabled
1b8ca024-1d5c-4dec-8995-b1a932b41780
Audit, Deny, DisabledSQL logical servers with public access on
Azure Key Vault should disable public network access
405c5871-3e91-4644-8a63-58e19d68ff5b
Audit, Deny, DisabledVaults reachable over the public internet

This is the guardrail where audit-first earns its keep twice. The audit pass is a free inventory of every public IP and publicly reachable service you own. And some of those are meant to be public — the web tier behind Front Door, say — so they get an exemption, category Mitigated, with the control that mitigates them written into the description. Deny broadly; open deliberately; write the reason down.

Guardrail four: diagnostic settings on everything

A resource with no diagnostic setting logs nothing, and you discover that mid-incident. Diagnostic settings are per resource, which means per forgettable, so this is the textbook deployIfNotExists case: check for the setting, deploy it if missing. You cannot deny a resource for lacking one, because the setting is a child created after the parent; audit would only give you a to-do list.

Microsoft ships this as built-in initiatives keyed on category groups: allLogs for everything, audit for audit logs only. Search the Monitoring category for Enable allLogs category group resource logging for supported resources to Log Analytics (or the audit variant, and the event hub and storage equivalents), each containing a per-resource policy such as Enable logging by category group for Key vaults (microsoft.keyvault/vaults) to Log Analytics. The parameters are the workspace, the category group, and optionally the list of resource types to cover.

# assign the initiative with an identity, supplying the workspace
az policy assignment create --name deploy-diagnostics \
  --policy-set-definition "<initiative id>" \
  --scope "/subscriptions/<sub>" \
  --params '{ "logAnalytics": { "value": "<workspace resource id>" } }' \
  --mi-system-assigned --location westeurope

# one remediation task per policy in the initiative
az policy remediation create --policy-assignment deploy-diagnostics \
  --name remediate-keyvault --definition-reference-id "<reference id>" \
  --resource-discovery-mode ReEvaluateCompliance

The identity needs a role that can create diagnostic settings at the scope — Microsoft names Monitoring Contributor or Log Analytics Contributor — and the assignment's roleDefinitionIds tell you exactly which. Once this is in place, the sign-in and activity logs you export actually cover the estate, with no blind spot where a resource quietly was not logging.

Guardrail five: a custom definition, when the rule is yours

Check the built-ins first; there are hundreds. You write a definition for an internal naming convention, a tag taxonomy or a configuration constraint nothing built-in expresses. Once you have read one, they all read the same way. Here is a complete one that flags storage accounts without HTTPS-only transport, with the effect parameterised so it can be assigned in audit and switched to deny without editing the definition:

{
  "properties": {
    "displayName": "Storage accounts must require secure transfer",
    "mode": "All",
    "parameters": {
      "effect": {
        "type": "String",
        "allowedValues": ["Audit", "Deny", "Disabled"],
        "defaultValue": "Audit"
      }
    },
    "policyRule": {
      "if": {
        "allOf": [
          { "field": "type", "equals": "Microsoft.Storage/storageAccounts" },
          { "field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly",
            "notEquals": "true" }
        ]
      },
      "then": { "effect": "[parameters('effect')]" }
    }
  }
}

The if matches on fields with equals, notEquals, exists, in, notIn and like (wildcards, which is how naming conventions are expressed), combined with allOf and anyOf. Create it, assign it in audit, read, then flip the parameter:

az policy definition create --name storage-secure-transfer \
  --display-name "Storage accounts must require secure transfer" \
  --rules "@rule.json" --params "@params.json" --mode All

az policy assignment create --name storage-secure-transfer \
  --policy storage-secure-transfer --scope "/subscriptions/<sub>" \
  --params '{ "effect": { "value": "Audit" } }'

# later, once the report reads clean
az policy assignment update --name storage-secure-transfer \
  --scope "/subscriptions/<sub>" --params '{ "effect": { "value": "Deny" } }'

If your custom definition uses modify or deployIfNotExists, it must list roleDefinitionIds under details using the full role resource ID, not the role name, or remediation has nothing to ask for. Bundle related definitions into an initiative once you have a handful; "our storage baseline" is one assignment, not six.

Policy, RBAC and Conditional Access: three planes, not one

Three services get confused for each other, and each answers a different question. RBAC answers who may do what to a resource; a Contributor can create a storage account. Azure Policy answers what may exist, configured how; that same Contributor's storage account is denied if it is in the wrong region, because Policy is evaluated regardless of how much permission the caller has. Conditional Access answers who may sign in, under what conditions — MFA, legacy authentication, device state — and it never looks at a resource. The full comparison with RBAC is in Azure Policy vs RBAC; the identity plane is covered by enforcing MFA with Conditional Access and blocking legacy authentication.

The order to roll it out

Standing an estate up from scratch, do the identity guardrails first, because that is where the highest-probability attack lives: MFA through Conditional Access, legacy authentication blocked. Wire up log export second, so you can see what is happening. Then layer Policy on in this order: diagnostic settings, because they make everything after them observable; allowed locations, because it is the cheapest deny and almost never wrong; tag inheritance, then tag requirements; public access last, because it is the one most likely to block something a team genuinely needs, and you want the audit inventory in hand before you enforce it. Every one of them goes in as audit first. Guardrails switched on all at once get switched off in a panic the first time one blocks a legitimate deploy on a Friday.

What to carry out of this

A definition is an if and a then. Effects run in a fixed order and stack to the most restrictive. Assign in audit, read, then deny. Existing resources are fixed by a remediation task under a managed identity that must hold the listed roles. Exempt with a reason; do not exclude. And keep Policy on the resource plane — sign-ins belong to Conditional Access.

Questions people also ask

What is Azure Policy used for?

Azure Policy enforces rules about what resources may exist in your Azure estate and how they must be configured. You assign a definition to a management group, subscription or resource group, and every resource in that scope is evaluated against it: created resources can be blocked or altered on the way in, and existing resources are reported compliant or non-compliant on a daily cycle. It governs resources, not sign-ins; who may log in is Conditional Access.

What is the difference between deny and audit in Azure Policy?

Deny blocks the create or update request, so the non-compliant resource never exists. Audit lets the request through and records the resource as non-compliant in the compliance report. Roll a new rule out as audit first, read what it flags, fix or exempt the surprises, then switch the same assignment to deny by changing its effect parameter.

What are the effects in Azure Policy?

Microsoft documents eleven: addToNetworkGroup, append, audit, auditIfNotExists, deny, denyAction, deployIfNotExists, disabled, manual, modify and mutate. The ones you will assign are audit, deny, modify, deployIfNotExists and auditIfNotExists. Each definition has exactly one effect, and it is usually exposed as a parameter so one definition can be assigned in audit and later switched to deny.

In what order does Azure Policy evaluate effects?

For a create or update request, disabled is checked first, then append and modify because they can change the request, then deny, then audit, then manual, then auditIfNotExists, with denyAction evaluated last. After the resource provider returns success, auditIfNotExists and deployIfNotExists run to decide whether more logging or a deployment is needed.

Does Azure Policy automatically remediate existing resources?

No. Resources created or updated after an assignment are handled at evaluation. Resources that already existed stay non-compliant until you create a remediation task, which runs the deployIfNotExists template or modify operations across them using the managed identity attached to the assignment. In the portal you can create the task at the same time as the assignment.

What permissions does Azure Policy remediation need?

The assignment carries a managed identity, and that identity must hold the roles listed in the definition’s roleDefinitionIds, for example Contributor or a narrower role. Assign through the portal and Azure grants those roles automatically; assign through the CLI, PowerShell or an SDK and you must grant them yourself, or the remediation fails on permissions.

What is the difference between an exemption and an exclusion in Azure Policy?

An exclusion, a notScope on the assignment, removes a scope from evaluation entirely, so the resources there are not counted. An exemption is a separate object on a resource or scope that keeps the resource in scope but marks it Exempt, with a category of Waiver or Mitigated, optional metadata such as a ticket reference, and an optional expiry. Exempt resources still count toward overall compliance, which is why exemptions are the auditable choice.

Do Azure resources inherit tags from their resource group?

No. Microsoft states that resources do not inherit the tags you apply to a resource group or a subscription. To carry a tag down, assign a modify policy such as Inherit a tag from the resource group if missing, which copies the value onto resources as they are created and, with a remediation task, onto resources that already exist.

How do I restrict which regions resources can be deployed to?

Assign the built-in Allowed locations policy with the list of approved regions. It denies resources created elsewhere, and its rule already ignores resources whose location is global and the Microsoft.AzureActiveDirectory/b2cDirectories type. Assign it as high as the rule is universally true, usually a management group, so new subscriptions inherit it.

Can Azure Policy enforce MFA?

No. Multifactor authentication is a sign-in control and belongs to Microsoft Entra Conditional Access. Azure Policy governs resources: it can deny a public IP, require a tag, restrict regions or deploy a diagnostic setting, but it does not evaluate sign-ins. Most governance confusion is one plane being asked to do the other plane’s job.

Further reading — the Microsoft docs
Your next class · free
You've read the idea. Class 8 — RBAC & Azure Policy is where you build it, hands-on — no account needed.Start Class 8 →
Captain O
Founder & instructor · CAMPUX Cloud Engineering Bootcamp
LinkedIn
Drilled in Class 8 — RBAC & Azure Policy. Back to all field notes →