Fixing Terraform Permission Errors in Azure
Terraform deployments against Azure frequently fail with permission errors when the service principal or managed identity running the deployment lacks the required RBAC roles. This page explains the two most common error types, how to diagnose exactly which permission is missing, and how to assign it correctly with Azure CLI.
The two permission error messages
During terraform apply, Azure returns HTTP 403 responses that Terraform surfaces as one of two error messages:
Error: creating/updating Resource Group "myapp-rg" (Subscription: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"):
resources.ResourceGroupsClient#CreateOrUpdate: Failure responding to request:
StatusCode=403 -- Original Error: autorest/azure: Service returned an error.
Status=403 Code="AuthorizationFailed"
Message="The client 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' with object id
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' does not have authorization to perform
action 'Microsoft.Resources/subscriptions/resourcegroups/write' over scope
'/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' or the scope is invalid."The second variant appears when Azure Policy blocks the action rather than RBAC:
Error: Code="RequestDisallowedByPolicy"
Message="Resource 'myapp' was disallowed by policy. Policy identifiers:
'[{\"policyAssignment\":{\"name\":\"Require tags on resources\",
\"id\":\"/subscriptions/.../policyAssignments/requiretags\"},
\"policyDefinition\":{\"name\":\"Require a tag on resources\"}}]'."These require different fixes. The AuthorizationFailed error means an RBAC role assignment is missing. The RequestDisallowedByPolicy error means an Azure Policy assignment is blocking the operation — adding RBAC roles will not fix it.
Identify which identity Terraform is using
Before assigning roles, confirm which identity is running Terraform. The error message includes the object ID, but you need to map that to a service principal or managed identity.
Find the service principal by its object ID:
az ad sp show --id "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
--query "{displayName: displayName, appId: appId, servicePrincipalType: servicePrincipalType}" \
--output jsonIf Terraform uses environment variables for authentication, check what is set:
# Typical Terraform environment variables for service principal auth
echo $ARM_CLIENT_ID
echo $ARM_TENANT_ID
echo $ARM_SUBSCRIPTION_ID
# ARM_CLIENT_SECRET should not be echoed but confirm it is set
printenv | grep ARM_List current role assignments for the service principal:
SP_OBJECT_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
az role assignment list \
--assignee "$SP_OBJECT_ID" \
--include-inherited \
--include-groups \
--output tableThe --include-inherited flag shows roles assigned at parent scopes (subscription, management group) that are inherited at the resource group level.
Required roles for common Terraform operations
The minimum role needed depends on what Terraform is creating:
| Operation | Required Role | Scope |
|---|---|---|
| Create/delete resource groups | Contributor | Subscription |
| Create most Azure resources | Contributor | Subscription or RG |
| Assign RBAC roles to resources | User Access Administrator | Scope being assigned |
| Create Azure AD apps / service principals | Application Administrator | Entra ID (directory level) |
| Register resource providers | Contributor | Subscription |
| Create management group assignments | Owner | Management Group |
Contributor alone is insufficient when Terraform creates a resource and also assigns a role on it (for example, creating a storage account and granting a managed identity Storage Blob Data Contributor). For that, the service principal needs both Contributor and User Access Administrator.
Assigning Owner to the Terraform service principal gives it the ability to grant any role to any principal, including itself. For production environments, prefer the combination of Contributor + User Access Administrator and scope both as narrowly as possible.
Assigning the missing role with Azure CLI
Once you know what role is needed, assign it to the service principal:
SP_OBJECT_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
SUBSCRIPTION_ID=$(az account show --query id -o tsv)
# Grant Contributor at subscription scope
az role assignment create \
--assignee "$SP_OBJECT_ID" \
--role "Contributor" \
--scope "/subscriptions/$SUBSCRIPTION_ID"
# Grant User Access Administrator at a specific resource group
az role assignment create \
--assignee "$SP_OBJECT_ID" \
--role "User Access Administrator" \
--scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/myapp-rg"After assigning the role, wait 2–5 minutes for the assignment to propagate, then retry terraform apply. Role assignment propagation is not immediate — retrying immediately after the assignment will still fail.
To verify the assignment took effect:
az role assignment list \
--assignee "$SP_OBJECT_ID" \
--scope "/subscriptions/$SUBSCRIPTION_ID" \
--include-inherited \
--output tableFinding which role covers a denied action
The error message includes the exact action that was denied, for example Microsoft.KeyVault/vaults/write. Use that action to find built-in roles that include it:
# Find roles that include a specific action
az role definition list \
--query "[?contains(permissions[0].actions, 'Microsoft.KeyVault/vaults/write')].{name: roleName}" \
--output tableFor a more precise search, use the --custom-role-only false flag to include all roles:
az role definition list \
--output json \
| python3 -c "
import json, sys
roles = json.load(sys.stdin)
action = 'Microsoft.KeyVault/vaults/write'
for r in roles:
for p in r.get('permissions', []):
actions = p.get('actions', [])
if any(action == a or a == '*' or
(a.endswith('*') and action.startswith(a[:-1]))
for a in actions):
print(r['roleName'])
break
"For custom Terraform operations that require a very narrow set of permissions, create a custom role rather than assigning broad built-in roles:
# Create a minimal custom role for Terraform
cat > tf-custom-role.json << 'EOF'
{
"Name": "Terraform Deployer",
"Description": "Minimum permissions for Terraform deployments in myapp-rg",
"Actions": [
"Microsoft.Resources/subscriptions/resourceGroups/*",
"Microsoft.Compute/virtualMachines/*",
"Microsoft.Network/*",
"Microsoft.Storage/storageAccounts/*"
],
"NotActions": [],
"AssignableScopes": [
"/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myapp-rg"
]
}
EOF
az role definition create --role-definition @tf-custom-role.jsonService principal secret expired
A common Terraform failure that looks like a permission error is actually an authentication failure from an expired service principal secret. The error appears differently:
Error: building AzureRM Client: obtain subscription() from Azure CLI:
Error parsing json result from the Azure CLI: Error waiting for the Azure CLI:
exit status 1: ERROR: AADSTS7000222: The provided client secret keys for app
'xxxxxxxx' are expired.Check the service principal’s credential expiry:
APP_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
az ad app credential list \
--id "$APP_ID" \
--query "[].{hint: hint, endDateTime: endDateTime}" \
--output tableRotate the secret:
# Create a new secret (valid for 1 year)
az ad app credential reset \
--id "$APP_ID" \
--years 1 \
--query "{clientId: appId, clientSecret: password}" \
--output jsonUpdate the ARM_CLIENT_SECRET environment variable or pipeline secret with the new value. The old secret continues working until its expiry date, so creating a new one does not break existing sessions.
Set a calendar reminder before service principal secrets expire. By default, secrets created in the portal last 1 year. Consider using federated credentials (workload identity federation) for CI/CD pipelines instead of secrets — federated credentials do not expire.
Common mistakes
- Assigning Contributor but forgetting User Access Administrator for RBAC assignments. When Terraform creates a resource and then assigns a role on it (a common pattern for managed identities), Contributor alone is not enough. User Access Administrator is required for any role assignment operation, even at a narrow scope.
- Retrying terraform apply immediately after adding a role assignment. Role assignments in Azure take 2–5 minutes to propagate through the authorization service. Retrying immediately will produce the same 403 error, leading engineers to think the role assignment failed when it actually just has not taken effect yet.
- Confusing AuthorizationFailed with RequestDisallowedByPolicy. These are different errors with different fixes. AuthorizationFailed requires adding an RBAC role. RequestDisallowedByPolicy requires modifying or exempting the Azure Policy assignment — RBAC cannot override a Deny policy effect.
- Using the Application Object ID instead of the Service Principal Object ID in role assignments. Every app registration has both an Application (app) object and a Service Principal object. Role assignments must use the Service Principal object ID, not the Application object ID. Use
az ad sp show —id <appId>to get the service principal object ID from the application ID.
Summary
- AuthorizationFailed during
terraform applymeans a missing RBAC role — read the error message to find the exact action denied, then assign the appropriate role to the service principal. - Contributor covers resource creation; User Access Administrator is additionally needed when Terraform assigns roles to other principals.
- Wait 2–5 minutes after adding a role assignment before retrying — Azure authorization propagation is not immediate.
- RequestDisallowedByPolicy is a separate error from AuthorizationFailed and requires modifying Azure Policy, not RBAC assignments.
Frequently asked questions
What RBAC role does a Terraform service principal need for most operations?
Contributor at the subscription or resource group scope covers most resource creation and management. If Terraform also needs to assign RBAC roles (e.g., granting a managed identity access to a storage account), it additionally needs User Access Administrator or Owner at the relevant scope.
How do I find the exact action that is being denied in a Terraform error?
The error message from Azure includes the action name, for example "does not have authorization to perform action 'Microsoft.KeyVault/vaults/write'". Copy that action name and use az role definition list --query to find which built-in role includes it, then assign that role.
Can I use a managed identity instead of a service principal for Terraform in Azure DevOps?
Yes. If Terraform runs on an Azure-hosted agent or a self-hosted agent on an Azure VM, you can use the VM's managed identity. Set use_msi = true in the azurerm provider configuration and remove the client_id and client_secret. The managed identity must have the same RBAC permissions as a service principal would.