Symptom
A deployment that worked in dev fails in production:
Code: RequestDisallowedByPolicy
Message: Resource 'stappprod01' was disallowed by policy. Reasons: 'Storage accounts should disable public network access'.
See error details for policy resource IDs.
Terraform shows the same as StatusCode=403 ... RequestDisallowedByPolicy. The short message is rarely enough. You need the assignment and the exact condition.
Step 1: get the full error details
The full error includes policyAssignmentId, policyDefinitionId and, for initiatives, policyDefinitionReferenceId.
az monitor activity-log list --resource-group rg-app-prod --offset 2h --status Failed \
--query "[?contains(properties.statusCode || '', 'Forbidden') || contains(to_string(properties), 'RequestDisallowedByPolicy')].properties.statusMessage" -o tsv
For ARM/Bicep deployments it's quicker to read the deployment operation:
az deployment operation group list -g rg-app-prod -n main \
--query "[?properties.provisioningState=='Failed'].properties.statusMessage.error.additionalInfo" -o json
Look for:
"policyAssignmentId": "/providers/Microsoft.Management/managementGroups/mg-landingzones/providers/Microsoft.Authorization/policyAssignments/Deny-Storage-Public",
"policyDefinitionReferenceId": "StoragePublicNetworkAccess",
"policyEffect": "deny"
Step 2: read the assignment and definition
az policy assignment show --ids "<policyAssignmentId>" \
--query "{name:displayName, scope:scope, enforcement:enforcementMode, params:parameters, notScopes:notScopes}"
az policy definition show --name "<definition-guid-or-name>" --query "policyRule" -o json
For an initiative (policySetDefinitions), find the member by its reference ID:
az policy set-definition show --name "<initiative-name>" \
--query "policyDefinitions[?policyDefinitionReferenceId=='StoragePublicNetworkAccess']"
Read the if block. It tells you exactly which property must be set. For example, publicNetworkAccess must equal Disabled.
Step 3: check what your template actually sends
The most common causes, in order:
- The template omits the property, and the API default violates the policy. Example: omitting
publicNetworkAccessmeans "Enabled" on many resource types. - The template sets an older
apiVersionwhere the property doesn't exist, so it never reaches the policy engine. - A child resource is created separately (a storage container, NSG rule, diagnostic setting) and the policy evaluates it on its own.
- A
denyActionpolicy blocks a delete (for example, deleting a resource taggedprotected).
Fix the template first. That is the right fix most of the time:
resource sa 'Microsoft.Storage/storageAccounts@2023-05-01' = {
name: 'stappprod01'
location: location
sku: { name: 'Standard_ZRS' }
kind: 'StorageV2'
properties: {
publicNetworkAccess: 'Disabled'
allowBlobPublicAccess: false
minimumTlsVersion: 'TLS1_2'
}
}
Step 4: if the resource genuinely needs an exception, use an exemption
Don't edit the assignment's notScopes for one resource. Use a time-limited exemption scoped to the single resource and, for initiatives, the single policy:
az policy exemption create -n exm-stappprod01-public \
--policy-assignment "<policyAssignmentId>" \
--policy-definition-reference-ids StoragePublicNetworkAccess \
--exemption-category Waiver \
--scope "/subscriptions/<sub>/resourceGroups/rg-app-prod/providers/Microsoft.Storage/storageAccounts/stappprod01" \
--expires-on 2027-01-31T00:00:00Z \
--description "CHG-12345: vendor SFTP needs public endpoint until private link GA"
Warning
Exemption at resource scope only works once the resource exists. For a new resource that's blocked on create, exempt the resource group temporarily, deploy, then narrow the exemption to the resource.
Use Mitigated instead of Waiver when the risk is covered another way. The category shows up in compliance reports.
Step 5: test before production next time
Assign the policy with enforcementMode DoNotEnforce in a test scope. It evaluates and reports compliance but doesn't block:
az policy assignment create -n test-deny-public --scope "/subscriptions/<test-sub>" \
--policy "<definition-id>" --enforcement-mode DoNotEnforce
Then list what would have been denied:
az policy state list --resource-group rg-app-test \
--filter "ComplianceState eq 'NonCompliant'" \
--query "[].{res:resourceId, policy:policyDefinitionReferenceId}" -o table
Troubleshooting table
| Symptom | Cause | Fix |
|---|---|---|
| Works in dev, fails in prod | Assignment at prod management group only | Assign the same initiative to dev in DoNotEnforce |
| Property is set, still denied | Old apiVersion doesn't send the property | Update the apiVersion |
| Deny on a child resource you didn't expect | Separate child resource evaluated | Set the property on the child, or exempt at its scope |
Delete blocked with denyAction | Protected tag or resource type | Remove the tag via change process, then delete |
| Exemption created, still denied | Wrong assignment ID or reference ID; multiple assignments deny the same thing | Check every assignment in the error details |
Prevent it next time
- Publish the landing zone policy list with the property each one requires, so app teams get it right in their templates.
- Run
az deployment group what-ifand aDoNotEnforceassignment in CI to catch policy conflicts before release. - Track exemptions with expiry dates; review them monthly.
- Related: Azure Policy remediation tasks.