Azure Deployment Stacks - What denyWriteAndDelete actually does to /action operations


Intro

When you enable denyWriteAndDelete on a deployment stack, the name makes it sound like a focused protection: block write and delete operations on stack-managed resources outside of your pipeline. That is what the documentation implies. What the platform actually does is broader — and if you don’t understand it before rolling out to production, your users will start hitting access errors that are not immediately obvious to diagnose.

This article covers what the deny assignment really looks like, how to inspect it, and how to build a methodology for finding everything you need to add to excludedActions before you flip the switch.

What the deny assignment actually looks like

The assumption is that denyWriteAndDelete creates a deny assignment that blocks */write and */delete. What it actually does is this:

Note: Microsoft does not document the internal structure of the deny assignment. The JSON below reflects what you actually see when you inspect it directly with az rest — treat that inspection command as your verification step, not the documentation.

{
  "actions": ["*"],
  "notActions": [
    "*/read",
    "...your excludedActions..."
  ]
}

The deny assignment denies everything — including /action operations — and uses notActions as an allowlist. Only */read and whatever you explicitly listed in excludedActions pass through.

This means operations like listClusterUserCredential/action or Microsoft.KeyVault/vaults/purge/action are silently blocked unless you add them by name.

How to inspect your deny assignments

After deploying a stack with denyWriteAndDelete, inspect the resulting deny assignment directly. This is the source of truth — not the documentation:

az rest \
  --method GET \
  --url "https://management.azure.com/subscriptions/<sub-id>/providers/Microsoft.Authorization/denyAssignments?api-version=2022-04-01" \
  --query "value[].{displayName:properties.displayName,actions:properties.permissions[0].actions,notActions:properties.permissions[0].notActions}" \
  -o json

Look for deny assignments where "actions": ["*"]. The notActions array is your effective allowlist — if an operation is not listed there, it is denied.

Run this after every stack deployment to verify exactly what is and is not allowed.

The excludedActions field is your allowlist

Your excludedActions in the deployment stack parameters maps directly to notActions on the deny assignment. The mental model is: everything is blocked except */read and whatever you list here.

Three implications:

  1. /action operations must be explicitly listed if users or Azure platform services need them.
  2. Wildcards workMicrosoft.Insights/diagnosticSettings/* covers both write and delete in one entry. Note that a wildcard also exempts any /action operations under that namespace, so use it only where the broader scope is acceptable.
  3. excludedPrincipals is separate — operations performed only by your deployment service principal don’t need to be in excludedActions. Use excludedPrincipals for that instead. Note that excludedPrincipals is capped at five principals; consolidate into Entra groups if you need to exclude more identities.

Note: excludedActions (mapped to DenySettingsExcludedAction in PowerShell / deny-settings-excluded-actions in CLI) is capped at 200 entries. Keep this in mind when building the list for environments with many resource types.

A methodology for finding what to exclude

Before enabling denyWriteAndDelete on a module, analyze activity logs to find operations that real users and Azure platform services actually perform on stack-managed resources.

Step 1 — KQL query against your management Log Analytics workspace

Run this over a 90-day window across the relevant subscriptions:

AzureActivity
| where TimeGenerated > ago(90d)
| where SubscriptionId in ("<your-subscription-ids>")
| where ActivityStatusValue in ("Success", "Failure")
| where OperationNameValue endswith "/write"
    or OperationNameValue endswith "/delete"
    or OperationNameValue endswith "/action"
| where Caller !in ("<your-deployment-sp-object-id>")
| summarize
    Count = count(),
    DaysActive = dcount(bin(TimeGenerated, 1d)),
    Callers = make_set(Caller, 10)
    by OperationNameValue, ActivityStatusValue
| order by Count desc

The Callers column will surface object IDs of both human users and Azure platform service principals. For any SP you do not recognize, look it up in Entra ID.

Step 2 — Identify Azure platform principals

Some operations in your activity logs come from Azure platform service principals, not your users. You can add them to excludedPrincipals, but it requires code logic to look up the service principal’s object ID in each tenant at deployment time — since object IDs differ per tenant, you cannot hardcode them portably. The simpler approach is to cover the operations they perform via excludedActions or a wildcard instead.

Common platform principals I have encountered:

IdentityWhat it does
NFV Resource ProviderRegular writes to Azure Firewall resources for platform health management
Azure Traffic ManagerDNS A record registration for private endpoints; firewall policy validation
Azure Traffic ManagerFirewall policy validate actions

Note: object IDs vary per tenant. Always look up the display name in your own Entra ID to confirm identity.

Step 3 — Cross-reference with stack-managed resource types

The deny assignment only applies to resources owned by the stack. Resources created by other resource providers — for example AKS node resource group VMs and scale sets — are not stack-managed and are not affected. Filter your activity log results to operations on resource types that appear in your Bicep templates.

Step 4 — Find /action operations through actual role assignments

Rather than enumerating every possible action in a resource provider manifest, a more targeted approach is to look at which roles are actually assigned on the subscription, and then inspect what /action permissions those roles grant.

First, pull all unique role definition IDs assigned on the subscription:

$subscriptionId = "<your-subscription-id>"

$roleAssignments = Get-AzRoleAssignment -Scope "/subscriptions/$subscriptionId"

$roleDefinitionIds = $roleAssignments.RoleDefinitionId | Sort-Object -Unique
$roleDefinitionIds

Then, for each role definition ID returned, look it up on AzAdvertizer. AzAdvertizer gives you a clear breakdown of all actions, notActions, dataActions, and notDataActions for every built-in Azure role — including every /action operation the role permits.

Work through each role assigned in your environment and note any /action entries. Those are the candidates that will be blocked by denyWriteAndDelete and may need to be added to excludedActions.

HINT

AzAdvertizer also lets you search by operation name if you already know a specific action you are trying to validate.

Any /action operation that a user with an assigned role can perform will be blocked by the deny assignment unless explicitly excluded.

Caveat: not all activity log operations are valid excludedActions

Some operations appear in activity logs but are not registered in the provider manifest. Attempting to add them to excludedActions results in:

does not match any of the actions supported by the providers

For example, Microsoft.Network/firewallPolicies/validate/action appears regularly in activity logs — Azure Traffic Manager performs health validation using it — but it cannot be referenced by that name in excludedActions. Options when you hit this:

  • Add the platform service principal to excludedPrincipals if the object ID is consistent in your tenant
  • Assess whether the blocked operation causes a meaningful user impact before taking any action

Summary

AssumptionReality
denyWriteAndDelete blocks */write and */deleteBlocks * — everything including /action
excludedActions supplements the denyexcludedActions is the allowlist (plus */read)
/action operations are safeMust be explicitly excluded if users need them
One-time analysis of write/delete ops is enoughAlso need to review /action permissions on all roles assigned in your environment

Final remark

Inspect the deny assignment directly after every stack deployment using the az rest command shown above. The notActions array is the actual source of truth for what is allowed. Do not rely on the excludedActions you wrote — verify that the platform rendered it correctly before users hit the restriction in production.