Skip to main content
Back
DateRead10 min

Every Azure Policy Effect Explained: Deny, Audit, Modify, and More

Key Takeaways

  • Only two effects need a managed identity: `modify` and `deployIfNotExists`. Every other effect, including denyAction and append, evaluates and acts without one, which is the fastest way to tell whether a definition will need remediation infrastructure at all.
  • `auditIfNotExists` and `deployIfNotExists` share almost the same schema. deployIfNotExists is auditIfNotExists plus roleDefinitionIds and a deployment template. If a definition only needs to flag a gap rather than fix it, auditIfNotExists is the whole mechanism, no identity required.
  • `denyAction` doesn't evaluate configuration at all. It blocks a specific action (only delete is supported today) on a resource that otherwise matches the if condition. It's also deliberately exempt for a fixed list of resource types, including policy assignments and resource locks, so it can't be used to lock an environment out of its own governance.
  • `az policy remediation create` and `Start-AzPolicyRemediation` don't expose the same controls. PowerShell's cmdlet has -FailureThreshold, -ResourceCount, and -ParallelDeploymentCount for tuning a remediation run. The current CLI command has no equivalents; it offers --resource-discovery-mode and --location-filters instead.

Every Azure Policy definition has exactly one effect, and how those effects evaluate relative to each other (disabled first, then append/modify, then deny, then audit, and so on) is covered in full in the main Azure Policy glossary entry. This page goes one level deeper: the schema and behavior of each effect, grouped by family, and how remediation works end to end for the two effects that need it.

Deny Effects: deny and denyAction

deny blocks a create or update request outright when the if condition matches. It has no details property at all, just the effect name.

denyAction differs in kind rather than degree. It doesn't evaluate a resource's configuration; it blocks a specific *action* against a resource that otherwise matches the if condition. The only supported action today is delete, which makes denyAction a defense against unwanted deletion rather than a configuration guardrail.

json
{
  "if": {
    "allOf": [
      {
        "field": "type",
        "equals": "Microsoft.DocumentDb/accounts"
      },
      {
        "field": "tags.environment",
        "equals": "prod"
      }
    ]
  },
  "then": {
    "effect": "denyAction",
    "details": {
      "actionNames": ["delete"],
      "cascadeBehaviors": {
        "resourceGroup": "deny"
      }
    }
  }
}

actionNames is a required array (delete is the only supported value), and cascadeBehaviors controls what happens when the protected resource would be deleted implicitly, as part of deleting its resource group. That property only applies in Indexed-mode definitions and defaults to deny. A few resource types are permanently exempt from denyAction enforcement by design, with Microsoft.Authorization/policyAssignments, Microsoft.Authorization/denyAssignments, Microsoft.Resources/subscriptions, and Microsoft.Authorization/locks among them, specifically so a denyAction policy can never be used to prevent someone from removing the very assignment or lock that would undo it. denyAction also doesn't block a subscription deletion, and it doesn't stop a cascade deletion from a parent resource down to a protected child or extension resource; it blocks only a direct delete call against the protected resource itself.

Audit Effects: audit and auditIfNotExists

audit logs a compliance warning based on the resource's own properties and, like deny, carries no details property.

auditIfNotExists audits based on a *related* resource's properties instead, using the same evaluation model as deployIfNotExists, minus the deployment. Its schema is type, name, resourceGroupName, existenceScope, evaluationDelay, and existenceCondition, the same properties that describe what deployIfNotExists looks for, minus roleDefinitionIds and a deployment template, because auditIfNotExists never creates anything. It runs after the Resource Provider returns success. If no related resource of type exists, or one exists but fails existenceCondition, the resource that matched the original if condition is marked non-compliant.

json
{
  "if": {
    "field": "type",
    "equals": "Microsoft.Compute/virtualMachines"
  },
  "then": {
    "effect": "auditIfNotExists",
    "details": {
      "type": "Microsoft.Compute/virtualMachines/extensions",
      "existenceCondition": {
        "allOf": [
          {
            "field": "Microsoft.Compute/virtualMachines/extensions/publisher",
            "equals": "Microsoft.Azure.Security"
          },
          {
            "field": "Microsoft.Compute/virtualMachines/extensions/type",
            "equals": "IaaSAntimalware"
          }
        ]
      }
    }
  }
}

This checks every VM for an antimalware extension and flags the ones missing it, without needing permission to install anything.

Append and Modify: Changing a Request Instead of Blocking It

append adds a field or value to a request before it reaches the Resource Provider, and it has only a details array. A plain field alias sets the field to the given value if it isn't already present; if the field already exists with a different value, append can't override it, and the request is denied instead of silently skipped. The [*] array alias behaves differently: it appends the value into a potentially pre-existing array without disturbing what's already there, creating the array if it doesn't exist yet. append needs no managed identity, which is why Microsoft recommends it as the fallback for tag policies when a managed identity isn't an option, or when modify doesn't yet support the alias you need.

modify is the more capable option for the same job, and Microsoft recommends it over append for tags specifically because it supports more operation types and can remediate resources that already exist. Its details require roleDefinitionIds (the role must cover everything the Contributor or Tag Contributor role grants) and an operations array, where each entry has an operation (addOrReplace, add, or remove, the last supported only for tags), a field, and, for anything but remove, a value:

json
{
  "then": {
    "effect": "modify",
    "details": {
      "roleDefinitionIds": [
        "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c"
      ],
      "conflictEffect": "deny",
      "operations": [
        {
          "operation": "remove",
          "field": "tags['env']"
        },
        {
          "operation": "addOrReplace",
          "field": "tags['environment']",
          "value": "[parameters('tagValue')]"
        }
      ]
    }
  }
}

conflictEffect (default deny) decides which policy definition's operations apply when more than one modify definition targets the same property, or when the target alias isn't modifiable in the request's API version. If exactly one of the conflicting definitions has conflictEffect: deny, its operations win and the others are skipped. If more than one has deny, the request is blocked as an unresolvable conflict. If all of them have audit, none of the operations run and the request goes through unmodified.

Modification also gets silently skipped, not applied and not flagged, in a few specific situations to check before debugging one:

  • On resources that already existed when the policy was assigned; they get marked non-compliant for a remediation task instead.
  • On an operation whose condition evaluates false.
  • When the target alias isn't modifiable in that request's API version.
  • When a nested property's parent isn't present in the request payload at all.
  • When the operation targets identity.type on anything other than a VM or Virtual Machine Scale Set.

The Rest: disabled, manual, mutate, and addToNetworkGroup

disabled skips evaluation entirely, which is useful for retiring one definition inside a shared initiative without deleting it. manual requires a person to attest compliance and never evaluates automatically. mutate is scoped to Microsoft.Kubernetes.Data mode, where it remediates AKS cluster components like pods at or before admission. addToNetworkGroup adds a resource to an Azure Virtual Network Manager network group rather than acting on the resource itself. None of these four need a managed identity, and none of them appear outside their specific contexts often enough to warrant their own section here; the main Azure Policy glossary entry covers where each sits in the overall evaluation order.

How Remediation Works

modify and deployIfNotExists only act automatically on resources created or updated after the assignment exists. Fixing resources that already existed beforehand takes a remediation task. Getting one to run correctly is a multi-step process that's easy to get wrong in a way that fails.

The policy definition needs roleDefinitionIds under details, an array of role IDs (not role names) that the remediation identity requires. Built-in policy definitions already have this populated; a custom definition has to declare it explicitly.

The managed identity performs the deployment or modification, and it's either system-assigned or user-assigned, one per assignment (though that one identity can hold multiple roles). Critically, the identity isn't used for everything: for deployIfNotExists, the caller who creates or updates the resource needs read access to check the existence condition, while the assignment's identity separately needs write access to deploy. Creating the identity through the Azure portal auto-grants it the roles listed in roleDefinitionIds. Creating it through the CLI, PowerShell, or an SDK doesn't. You have to do that manually with az role assignment create or New-AzRoleAssignment against each role ID in the definition. Changing a policy definition's roleDefinitionIds later doesn't retroactively update any existing assignment's identity either; you have to grant the new permissions by hand every time.

Creating the remediation task is where CLI and PowerShell diverge in more than syntax:

bash
az policy remediation create \
  --name fix-missing-tde \
  --policy-assignment require-sql-tde \
  --resource-discovery-mode ReEvaluateCompliance
Start-AzPolicyRemediation -Name 'fix-missing-tde' `
  -PolicyAssignmentId '/subscriptions/{subscriptionId}/providers/Microsoft.Authorization/policyAssignments/require-sql-tde' `
  -ResourceCount 1000 `
  -ParallelDeploymentCount 20 `
  -FailureThreshold 10

--resource-discovery-mode is CLI-only and takes ExistingNonCompliant (the default, remediate what the last compliance scan already flagged) or ReEvaluateCompliance (re-check compliance first, catching anything that's drifted since the last scan, before remediating). PowerShell's three tuning parameters have no CLI equivalent at all:

  • -ResourceCount caps how many non-compliant resources one task touches (default 500, maximum 50,000).
  • -ParallelDeploymentCount controls how many resources it fixes at once (1 to 30, default 10).
  • -FailureThreshold sets what percentage of failures aborts the task (default 100%, meaning it doesn't abort on partial failure at all).

A team standardized on the Azure CLI for everything else will need PowerShell, the REST API, or an SDK specifically to tune a large or sensitive remediation run.

Once a task is running, az policy remediation show and list report progress, and az policy remediation cancel stops one in flight. Resources touched by a remediation task also show up on the assignment's own Deployed Resources tab in the portal.

Common Mistakes

Treating `denyAction` as a configuration guardrail. It blocks the delete action specifically. It does nothing to stop the same resource from being misconfigured in every other way; that's still the job of deny or audit.

Assuming `append` is always non-blocking. A plain-alias append that would overwrite an existing, different value acts like deny and rejects the request. It's only silent-and-safe when the field doesn't already exist or when you use the [*] array alias.

Assuming `az policy remediation create` has the same tuning knobs as PowerShell. It doesn't expose resource count, parallelism, or a failure threshold. A team that needs to control the blast radius of a large remediation run has to reach for Start-AzPolicyRemediation or the REST API instead.

Changing `roleDefinitionIds` on a policy definition and assuming existing assignments pick it up. They don't. Every assignment's managed identity keeps whatever roles it was granted at creation time until someone manually grants the new ones.

Leaving `conflictEffect` at its default on a `modify` policy that touches a commonly modified property. The default is deny, so a second, unrelated modify definition editing the same field can turn the request into an outright block instead of the audit-and-move-on behavior most teams expect.

Best Practices

Pilot with auditIfNotExists before committing to deployIfNotExists on the same condition; it needs no managed identity and no remediation infrastructure to tell you how many resources would be affected. For tag enforcement specifically, default to modify and only fall back to append when a managed identity isn't available or the alias you need isn't modifiable yet. Set conflictEffect to audit on modify definitions that reference aliases, per Microsoft's own recommendation, so a property that isn't modifiable in one API version doesn't turn into a hard failure in production. Re-grant managed identity roles by hand every time a custom policy definition's roleDefinitionIds changes, since nothing does it automatically outside the portal. Finally, use --resource-discovery-mode ReEvaluateCompliance when remediating shortly after a related configuration change, rather than trusting a compliance scan that may already be stale.

Why Native

Native tracks which Azure Policy assignments have a working remediation path (managed identity granted, roles current) and which ones look enforced on paper but would silently fail the moment a remediation task runs. See how Native operationalizes built-in cloud controls instead of just reporting on them.

FAQ

What's the difference between `audit` and `auditIfNotExists`?

audit evaluates the resource's own properties. auditIfNotExists evaluates whether a *related* resource, like an extension or a diagnostic setting, exists and meets a condition, and flags the original resource as non-compliant if it doesn't.

Does `denyAction` stop someone from misconfiguring a resource?

No. denyAction only blocks a specific action, currently just delete, against a resource. It has no visibility into configuration at all; you still need a deny or audit effect to govern how the resource is set up.

Why did my `append` policy deny a request instead of just adding a field?

A plain-alias append acts like deny if the field already exists with a different value than the one being appended. Use the [*] array alias if you want to add to a potentially pre-existing array without overwriting it.

Do I need a managed identity for every policy effect?

No. Only modify and deployIfNotExists require one, since they're the only effects that change or create something. deny, audit, append, auditIfNotExists, denyAction, disabled, and manual all evaluate without one.

Can I control how many resources a remediation task processes at once?

Yes, but only through PowerShell's Start-AzPolicyRemediation (-ResourceCount, -ParallelDeploymentCount) or the REST API. The current az policy remediation create command doesn't expose those controls.

What happens if I change a policy definition's `roleDefinitionIds` after an assignment already exists?

Nothing happens automatically. The assignment's managed identity keeps its original roles until someone manually grants the new ones, through Access Control (IAM) in the portal or with az role assignment create / New-AzRoleAssignment.

Ready to enforce secure-by-design?