Skip to main content
Back
DateRead13 min

Azure Policy Explained: How It Works, With Real Examples and Best Practices

Key Takeaways

  • Azure Policy decides whether a resource's configuration is allowed to exist. It doesn't decide who's allowed to request it. Azure RBAC governs identity and actions. Azure Policy evaluates resource properties against a rule regardless of who's asking, and it still blocks a non-compliant create or update even when the requester has full RBAC rights to make that change.
  • Effects evaluate in a fixed order, and that order creates a real gotcha. append and modify run before deny and audit in the evaluation sequence, so a modify effect that alters a field can quietly prevent a deny or audit from ever firing on the value that was submitted.
  • Layering is cumulative and explicit-deny rather than nearest-wins. A more permissive assignment at a child scope can't override a deny set higher in the management group hierarchy. Anyone applying a GCP Organization Policy mental model here, where a boolean constraint lets a child override its parent, will misjudge what's enforced.
  • Without extra work, deployIfNotExists and modify only reach resources going forward. New and updated resources get evaluated automatically. Existing non-compliant resources need an explicit remediation task, and that task's managed identity needs RBAC roles that the CLI and SDK don't grant automatically the way the portal does.

What Is Azure Policy

A principal with full Owner rights on a subscription can still be blocked from creating a resource, not because of anything wrong with their permissions, but because Azure Policy evaluates the resource itself and doesn't check who's asking. Azure Policy is Microsoft's cloud governance service, one of the native guardrail mechanisms cloud providers ship rather than something a security team bolts on separately: it evaluates the properties of a resource being created, updated, or already running in Azure against JSON-defined rules called policy definitions, and applies an effect when the resource matches, denying the request, logging it, altering it, or deploying a related resource. Microsoft draws the line against Azure RBAC directly: Azure Policy "ensures that resource state is compliant to your business rules without concern for who made the change or who has permission to make a change." RBAC decides who can act. Policy decides whether the result is allowed to exist, and the two checks run independently of each other.

Policy definitions can be grouped into an initiative (Microsoft's SDKs still call this object a policySet) so a set of related rules (everything mapping to a specific compliance framework) can be assigned and tracked as a single unit rather than as dozens of separate assignments.

Where Azure Policy Sits: Scope, Hierarchy, and What Gets Evaluated

Policy definitions and assignments live in Azure's resource hierarchy:

Text
Tenant
  └── Management Group (can nest several levels deep)
        └── Management Group
              └── Subscription
                    └── Resource Group
                          └── Resource

A definition's location, a management group or a subscription, determines what it can be assigned to: a definition created at a subscription can only be assigned within that subscription, while one created at a management group can be assigned to any child management group or subscription beneath it. An assignment applies to everything at its scope and below, and you can carve a child scope out of that assignment with notScopes.

Although a policy assignment can be set at the management group level, only resources at the subscription or resource group level are evaluated. The management group itself isn't a resource Azure Policy inspects; it's the scope the assignment cascades down from.

Separate from what Policy enforces on other resources, a specific set of RBAC roles governs Policy itself. Managing policy objects requires permissions in the Microsoft.Authorization and Microsoft.PolicyInsights resource providers. Resource Policy Contributor covers most policy operations; Owner has full rights; Contributor can trigger remediation but can't create or update definitions and assignments; and granting RBAC roles to the managed identity used by deployIfNotExists or modify requires User Access Administrator. Every policy object (definitions, initiatives, and assignments) is readable by any role holder at that scope and below, by design, regardless of whether they have permission to change it.

How a Policy Definition and Assignment Work

A policy definition is JSON with a handful of top-level elements: displayName, description, mode, version, metadata, parameters, and the policyRule itself, which is an if condition and a then effect.

A deny example, blocking storage accounts that allow traffic over plain HTTP:

json
{
  "properties": {
    "displayName": "Require HTTPS-only traffic on storage accounts",
    "description": "Denies storage accounts that don't enforce secure transfer.",
    "mode": "Indexed",
    "policyRule": {
      "if": {
        "allOf": [
          {
            "field": "type",
            "equals": "Microsoft.Storage/storageAccounts"
          },
          {
            "field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly",
            "equals": "false"
          }
        ]
      },
      "then": {
        "effect": "deny"
      }
    }
  }
}

mode determines which resource types get evaluated at all: Indexed only evaluates resource types that support tags and location, while all evaluates every resource type, including resource groups and subscriptions themselves. Microsoft recommends all in most cases; Indexed exists specifically so policies enforcing tags or locations don't report resource types that can't carry a tag as falsely non-compliant. A handful of Resource Provider modes extend this further for deeper integrations (Microsoft.Kubernetes.Data for AKS and Arc-enabled clusters among them), each limited to a smaller set of effects than a standard resource policy gets.

An assignment is a separate JSON object that points a definition or initiative at a scope and supplies its parameter values:

json
{
  "properties": {
    "displayName": "Require HTTPS-only traffic on storage accounts - Production",
    "policyDefinitionId": "/subscriptions/{subId}/providers/Microsoft.Authorization/policyDefinitions/require-https-storage",
    "definitionVersion": "1.*.*",
    "enforcementMode": "Default",
    "notScopes": [
      "/subscriptions/{subId}/resourceGroups/legacy-migration-rg"
    ]
  }
}

enforcementMode is the assignment-level equivalent of a dry run: Default enforces the effect, DoNotEnforce evaluates and reports compliance without denying anything or writing to the Activity log, and Enroll makes the assignment available for staged rollout through separate enrollment resources scoped underneath it. Two more assignment properties solve two other operational problems. resourceSelectors lets an assignment apply only to a subset of resources, by location or resource type, so a new or risky assignment can roll out gradually instead of hitting an entire scope on day one. overrides lets you swap a policy's effect at assignment time (say, from audit to disabled) without editing the underlying definition, which matters most on an initiative where dozens of policy definitions would otherwise need editing individually.

Creating and assigning the definition above from the CLI. The --rules flag takes just the if/then block, not the full properties object, so policy-rule.json here holds only the if/then portion of the JSON shown above:

bash
az policy definition create \
  --name require-https-storage \
  --display-name "Require HTTPS-only traffic on storage accounts" \
  --mode Indexed \
  --rules @policy-rule.json
 
az policy assignment create \
  --name require-https-storage-prod \
  --display-name "Require HTTPS-only traffic on storage accounts - Production" \
  --scope "/subscriptions/{subId}" \
  --policy require-https-storage \
  --enforcement-mode Default

Effects and the Order They Evaluate In

Every policy definition has exactly one effect. The current supported effects are:

EffectWhat it does
denyBlocks the create or update request outright
auditLogs a compliance warning; doesn't block the request
appendAdds a field or value to the request before it's processed
modifyAdds, updates, or removes a field on the resource; requires a managed identity
deployIfNotExistsDeploys a related resource if it doesn't already exist; requires a managed identity
auditIfNotExistsLogs non-compliance if a related resource doesn't already exist
disabledThe rule isn't evaluated at all; useful for retiring a definition inside a shared initiative without deleting it
manualRequires a person to manually attest compliance; doesn't evaluate automatically
denyActionBlocks a specific action (like a delete) on a resource; independent of its configuration
mutateUsed in Microsoft.Kubernetes.Data mode to remediate AKS cluster components, like pods, before or at admission
addToNetworkGroupAdds a resource to an Azure Virtual Network Manager network group

These don't evaluate in the order they're written. Azure Policy checks disabled first, then append and modify, then deny, then audit, then manual, then auditIfNotExists, and denyAction last, before the request ever reaches the Resource Provider. Only after the Resource Provider returns success does Azure Policy evaluate auditIfNotExists and deployIfNotExists against the now-created resource. The practical consequence: because modify runs before deny and audit, a modify policy that silently corrects a field can prevent a deny or audit policy watching that same field from ever triggering, since by the time those effects evaluate, the value has already changed.

Not every effect is interchangeable. audit, deny, and either modify or append are commonly parameterized so one definition can be reused with a different effect per assignment. auditIfNotExists and deployIfNotExists pair the same way. manual isn't interchangeable with anything, and disabled can substitute for any of them.

How Multiple Assignments Layer

Several assignments, at the same scope or different ones, frequently apply to the same resource, and each is evaluated independently. Microsoft's own example makes the mechanism concrete: a deny policy at a subscription restricts resources to westus, while an audit policy at a resource group inside that subscription flags anything not in eastus. A resource in that resource group, in eastus, is compliant with the audit policy and non-compliant with the deny policy at the same time, and the deny policy is still what blocks a new resource from landing anywhere but westus. If both policies used deny instead, the outcome would be described as cumulative most restrictive: a new resource in that resource group would be blocked entirely, because it can't simultaneously satisfy westus and eastus.

This is the detail that trips up anyone moving between Azure and Google Cloud. Azure Policy is an explicit-deny system: a more permissive assignment at a child scope can't override a deny set at a parent. If a management-group-level assignment denies a resource type outright and a subscription underneath it needs to allow that type, the fix is to exclude that subscription from the parent assignment with notScopes and then assign the permissive definition directly to it. GCP Organization Policy works differently for boolean constraints: the nearest node to the resource wins, so a child can override what a parent set. Treating Azure Policy's hierarchy as if it worked the same way is a common and avoidable multi-cloud mistake.

Azure Policy Examples

A `modify` effect, shown as the same if/then fragment --rules expects, that adds a required tag with a default value, mirroring one of Microsoft's own built-ins:

json
{
  "if": {
    "field": "tags['CostCenter']",
    "exists": "false"
  },
  "then": {
    "effect": "modify",
    "details": {
      "roleDefinitionIds": [
        "/providers/microsoft.authorization/roleDefinitions/4a9ae827-6dc8-4573-8ac7-8239d42aa03f"
      ],
      "operations": [
        {
          "operation": "add",
          "field": "tags['CostCenter']",
          "value": "unassigned"
        }
      ]
    }
  }
}

A `deployIfNotExists` effect, adapted from Microsoft's canonical example, enabling transparent data encryption on any SQL database that doesn't already have it:

json
{
  "if": {
    "field": "type",
    "equals": "Microsoft.Sql/servers/databases"
  },
  "then": {
    "effect": "deployIfNotExists",
    "details": {
      "type": "Microsoft.Sql/servers/databases/transparentDataEncryption",
      "name": "current",
      "evaluationDelay": "AfterProvisioning",
      "existenceCondition": {
        "field": "Microsoft.Sql/transparentDataEncryption.status",
        "equals": "Enabled"
      },
      "roleDefinitionIds": [
        "/providers/Microsoft.Authorization/roleDefinitions/{builtinroleGUID}"
      ],
      "deployment": {
        "properties": {
          "mode": "incremental",
          "template": {
            "resources": [
              {
                "name": "[concat(parameters('fullDbName'), '/current')]",
                "type": "Microsoft.Sql/servers/databases/transparentDataEncryption",
                "apiVersion": "2014-04-01",
                "properties": { "status": "Enabled" }
              }
            ]
          },
          "parameters": {
            "fullDbName": { "value": "[field('fullName')]" }
          }
        }
      }
    }
  }
}

Two identity details matter here and are easy to get backward. The assignment's managed identity performs the actual template deployment. The requester's identity, not the assignment's, is what's used to evaluate the existenceCondition at the moment the SQL database is created or updated. That split means the person or service creating the resource needs read access to check whether the related resource already exists, while the assignment's identity separately needs write access to create it.

Counting array members, checking that a network security group has no more than one rule matching a given description. This is a pattern that comes up constantly in real custom policies and that a lot of Azure Policy content skips entirely:

json
{
  "count": {
    "field": "Microsoft.Network/networkSecurityGroups/securityRules[*]",
    "where": {
      "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].description",
      "equals": "Temporary access rule"
    }
  },
  "greaterOrEquals": 2
}

Paired with an audit or deny effect, this flags an NSG the moment a second rule with that description shows up, the kind of drift a point-in-time review would only catch after the fact.

Azure Policy Limits and Quotas

Current as of this writing; check the source before designing close to a limit, since Microsoft adjusts these over time.

ObjectScopeMaximum
Policy definitionsManagement group or subscription500
Initiative definitionsManagement group or subscription200
Initiative definitionsTenant2,500
Policy or initiative assignmentsAny scope200
ExemptionsAny scope1,000
ParametersPer policy definition20
Policy definitionsPer initiative1,000
ParametersPer initiative400
Exclusions (notScopes)Per assignment400
Nested conditionalsPer policy rule512
ResourcesPer remediation task50,000

Common Mistakes

Leaving `mode` set to `Indexed` when enforcing tags or a location on a resource group or subscription itself. Indexed mode skips resource types that don't support tags or location, and a resource group or subscription is exactly the kind of target that gets silently skipped. Enforcing a tag requirement on the resource group itself, not just the resources inside it, needs mode: all and a condition that explicitly targets the Microsoft.Resources/subscriptions/resourceGroups type.

Not accounting for `modify` and `append` running before `deny` and `audit` in the same initiative. If a modify policy corrects a field and a deny or audit policy in the same initiative is watching that same field, the deny or audit effect evaluates against the corrected value, not the one that was submitted, and never fires. If a resource that should be getting flagged isn't, check whether a modify or append policy earlier in the same initiative is quietly fixing the field before the audit ever sees it.

Creating a `deployIfNotExists` or `modify` assignment outside the portal and forgetting the managed identity's roles. The portal auto-grants the roleDefinitionIds listed in the policy definition to the assignment's managed identity. The CLI, PowerShell, and SDK don't. Skip that step outside the portal and remediation deployments fail with a permissions error that has nothing to do with the policy logic itself.

Confusing an exclusion with an exemption. notScopes on an assignment is a structural carve-out set at assignment time, permanent until someone edits the assignment. A policy exemption is a separate, time-bound object attached to a specific resource, categorized as Waiver or Mitigated, that keeps the resource counted in compliance reporting with a distinct Exempt state rather than silently removing it from evaluation. A resource excluded via notScopes disappears from the picture; an exempted resource stays visible with a documented reason and, optionally, an expiration.

Assuming new resources are protected the instant an assignment exists, and stopping there. New and updated resources get evaluated automatically going forward. Resources that already existed before the assignment or that predate a deployIfNotExists/modify policy don't get fixed on their own; that requires an explicit remediation task, and by default a single task caps out at 50,000 resources.

Best Practices

Start every new assignment with audit or auditIfNotExists before moving to deny, modify, or deployIfNotExists, exactly as Microsoft recommends: an enforcement effect can break an automation pipeline that was working fine under the radar. Define policies at the management group or subscription level, and assign at the next level down, so the same definition can be reused with different parameters per team without duplicating logic. Group definitions into an initiative even when starting with just one, since adding a second related policy later doesn't require a second assignment to track. Use resourceSelectors to stage a new or materially changed assignment across a subset of locations or resource types before opening it to the full scope, rather than finding out what breaks across an entire subscription at once. Reach for a policy exemption rather than a permanent notScopes exclusion, when a resource has a genuine temporary reason to be non-compliant: an exemption keeps the resource visible in compliance reporting with an owner and, ideally, an expiration, while a silent exclusion tends to outlive the reason it was created for. Manage policy definitions, initiatives, and assignments as code with reviewed changes, the same discipline you'd apply to any other infrastructure that governs production.

Why Native

Native maps Azure Policy assignments and effects across an environment alongside the equivalent controls in AWS, Google Cloud, and OCI, so a security team can close the gaps where a rule enforced in one provider has no counterpart, or only a weaker one, in another. See how Native aligns controls across every cloud you run.

FAQ

What's the difference between Azure Policy and Azure RBAC?

Azure RBAC controls which principals can perform which actions on which resources. Azure Policy controls whether a resource's configuration is allowed to exist, independent of who requested it, and it still blocks a non-compliant change even from a principal with full RBAC permissions to make it.

Can a subscription-level assignment override a deny set at the management group above it?

No. Azure Policy layers as cumulative most restrictive, and a deny at a parent scope can't be loosened by a more permissive assignment at a child scope. The fix is to exclude the child scope from the parent assignment with notScopes, then assign the more permissive definition directly to it.

What's the difference between an exclusion and an exemption?

An exclusion (notScopes on an assignment) is a permanent structural carve-out set when the assignment is created. An exemption is a separate, typically time-bound object attached to a specific resource, categorized as a waiver or a mitigation, that keeps the resource tracked in compliance reporting with an Exempt state instead of removing it from evaluation entirely.

Why didn't my `deployIfNotExists` policy fix resources that already existed?

deployIfNotExists and modify only act automatically on resources created or updated after the assignment exists. Resources that already existed beforehand need an explicit remediation task, which also requires the assignment's managed identity to already hold the RBAC roles listed in the policy definition's roleDefinitionIds.

What's the difference between a policy definition and an initiative?

A policy definition is a single rule with a single effect. An initiative, referred to as a policySet in Microsoft's SDKs, groups multiple policy definitions under one assignment so they can be managed and tracked together, commonly used to represent an entire compliance framework as one unit.

Is Azure Policy compliance evaluated in real time or on a schedule?

Both. A resource is evaluated when it's created or updated, when a new assignment is made, when an existing assignment or definition is changed, and on a standing compliance cycle that runs about once every 24 hours. An on-demand evaluation can also be triggered manually with az policy state trigger-scan.

Can I test an Azure Policy assignment before it enforces anything?

Yes. Setting enforcementMode to DoNotEnforce evaluates the assignment and reports compliance without denying any request or writing an entry to the Activity log, which is the same dry-run pattern to use before any new deny, modify, or deployIfNotExists assignment goes live.

Ready to enforce secure-by-design?