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 json

If 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 table

The --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:

OperationRequired RoleScope
Create/delete resource groupsContributorSubscription
Create most Azure resourcesContributorSubscription or RG
Assign RBAC roles to resourcesUser Access AdministratorScope being assigned
Create Azure AD apps / service principalsApplication AdministratorEntra ID (directory level)
Register resource providersContributorSubscription
Create management group assignmentsOwnerManagement 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.

Warning

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 table

Finding 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 table

For 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.json

Service 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 table

Rotate 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 json

Update 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.

Tip

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.

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.

Last verified: 19 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.