Skip to content
DevOps AI ToolKit
Newsletter
All guides
Azure with AI By James Joyner IV · · 10 min read

Azure Error Guide: 'Forbidden' Key Vault Secrets Get Permission Denied

Fix the Azure Key Vault Forbidden / 'does not have secrets get permission' error: diagnose access policies vs RBAC, wrong identity, firewall rules, and tenant mismatches.

  • #azure
  • #troubleshooting
  • #errors
  • #keyvault

Overview

A Key Vault Forbidden error happens when the caller successfully authenticates to Entra ID but is not authorized to read the secret. The vault’s permission model — either legacy access policies or Azure RBAC — has no entry granting the caller secrets get, or a network rule blocks the request before the data-plane check. Azure returns 403 and the application fails to start.

You will see this in the SDK exception or az output:

ERROR: (Forbidden) The user, group or application 'appid=8f3b2c1d-aaaa-bbbb-cccc-1234567890ab;oid=2c4e6a8b-...;iss=https://sts.windows.net/<tenant>/' does not have secrets get permission on key vault 'prod-kv;location=eastus'. For help resolving this issue, please see https://go.microsoft.com/fwlink/?linkid=2125287

And in the activity log / SDK error body for an RBAC-mode vault:

{"error":{"code":"Forbidden","message":"Caller is not authorized to perform action on resource.\nIf role assignments, deny assignments or role definitions changed recently, please observe propagation time.\nCaller: appid=8f3b2c1d-aaaa-bbbb-cccc-1234567890ab;oid=2c4e6a8b-...\nAction: 'Microsoft.KeyVault/vaults/secrets/getSecret/action'\nResource: '/subscriptions/<sub>/resourcegroups/rg-prod/providers/microsoft.keyvault/vaults/prod-kv/secrets/db-password'","innererror":{"code":"ForbiddenByRbac"}}}

It occurs at the data plane — after a token is issued — so authentication is fine. The failure is purely an authorization gap on the vault, and the exact cause depends on whether the vault uses access policies or RBAC.

Symptoms

  • App throws Forbidden / 403 reading a secret, but the same identity can log in fine.
  • az keyvault secret show returns (Forbidden) ... does not have secrets get permission.
  • The error references an appid/oid that does not match who you think is calling.
  • Works from one machine/network but fails from another (firewall).
az keyvault secret show --vault-name prod-kv --name db-password --query value -o tsv
ERROR: (Forbidden) The user, group or application 'appid=8f3b2c1d-...;oid=2c4e6a8b-...' does not have secrets get permission on key vault 'prod-kv;location=eastus'.
Code: Forbidden
az keyvault show --name prod-kv --query "properties.enableRbacAuthorization" -o tsv
true

The enableRbacAuthorization flag tells you which permission model to debug — true means RBAC, false/empty means access policies.

Common Root Causes

1. Missing access policy (vault in access-policy mode)

When enableRbacAuthorization is false, authorization comes from properties.accessPolicies. If the caller’s object ID has no policy granting secrets get, every read is Forbidden.

az keyvault show --name prod-kv \
  --query "properties.accessPolicies[].{oid:objectId, secrets:permissions.secrets}" -o table
Oid                                   Secrets
------------------------------------  ------------------
1a1a1a1a-bbbb-cccc-dddd-eeeeeeeeeeee  ['get', 'list']

The caller’s oid (2c4e6a8b-...) is not in the list — no policy, so no access.

2. Missing RBAC role (vault in RBAC mode)

When enableRbacAuthorization is true, access policies are ignored entirely. The caller needs a role like Key Vault Secrets User assigned at the vault (or higher) scope.

VAULT_ID=$(az keyvault show --name prod-kv --query id -o tsv)
az role assignment list --scope "$VAULT_ID" \
  --query "[].{principal:principalId, role:roleDefinitionName, scope:scope}" -o table
Principal                             Role                          Scope
------------------------------------  ----------------------------  --------------------------------
1a1a1a1a-bbbb-cccc-dddd-eeeeeeeeeeee  Key Vault Administrator       .../vaults/prod-kv

The caller’s principal ID is absent. A Reader role does NOT grant data-plane access — only data-plane roles like Key Vault Secrets User work.

3. Wrong identity (managed identity vs user)

The error’s appid/oid reveals who actually called. A common trap: you granted access to your user account but the app runs under a managed identity (or vice versa), so the identity in the policy never matches the caller.

# What identity is the error referencing? Resolve the oid:
az ad sp show --id 2c4e6a8b-1111-2222-3333-444455556666 \
  --query "{name:displayName, type:servicePrincipalType, appId:appId}" -o table
Name                       Type           AppId
-------------------------  -------------  ------------------------------------
prod-app-mi                ManagedIdentity 8f3b2c1d-aaaa-bbbb-cccc-1234567890ab

If the access policy/role was granted to john@contoso.com but the caller is the prod-app-mi managed identity, authorization fails — grant it to the MI’s object ID.

4. Firewall / private endpoint blocking the caller

Even with correct permissions, the vault’s network rules can reject the request before the data plane evaluates it. A vault with defaultAction: Deny blocks any source IP/VNet not explicitly allowed.

az keyvault network-rule list --name prod-kv \
  --query "{default:defaultAction, ips:ipRules[].value, vnets:virtualNetworkRules[].id}" -o jsonc
{
  "default": "Deny",
  "ips": [ "203.0.113.10/32" ],
  "vnets": [ "/subscriptions/<sub>/.../subnets/aks-subnet" ]
}

If the caller’s egress IP or subnet is not in the allow list, it gets a 403 (or ForbiddenByFirewall in the inner error) regardless of policy/RBAC.

5. Purge-protection / soft-delete state on the secret

A secret can be present but in a soft-deleted or disabled state, which surfaces as access errors. A disabled secret version cannot be read, and a deleted secret returns Forbidden/SecretNotFound until recovered.

az keyvault secret show --vault-name prod-kv --name db-password \
  --query "attributes.enabled" -o tsv
az keyvault secret list-deleted --vault-name prod-kv \
  --query "[?name=='db-password'].{name:name, recoveryId:recoveryId}" -o table
false
Name         RecoveryId
-----------  -----------------------------------------------------------
db-password  https://prod-kv.vault.azure.net/deletedsecrets/db-password

The secret is disabled (enabled: false) or sitting in the deleted state — re-enable or recover it.

6. Wrong tenant

If you authenticated against the wrong tenant, the token’s iss (issuer) points at a different directory than the vault, and the caller’s object ID does not exist in the vault’s tenant. The error’s iss=https://sts.windows.net/<tenant>/ reveals the mismatch.

az account show --query "{tenantId:tenantId, user:user.name, sub:name}" -o table
az keyvault show --name prod-kv --query "properties.tenantId" -o tsv
TenantId                              User                Sub
------------------------------------  ------------------  ----------------
99999999-aaaa-bbbb-cccc-dddddddddddd  john@contoso.com    Contoso Prod
11111111-aaaa-bbbb-cccc-dddddddddddd

The logged-in tenant (9999...) differs from the vault’s tenant (1111...) — no identity in the wrong tenant can be authorized. Re-login with az login --tenant <vault-tenant>.

Diagnostic Workflow

Step 1: Determine the vault’s permission model

az keyvault show --name <VAULT> \
  --query "{rbac:properties.enableRbacAuthorization, tenant:properties.tenantId, net:properties.networkAcls.defaultAction}" -o jsonc

rbac: true means debug RBAC; otherwise debug access policies. Note the tenant and network default action.

Step 2: Identify the real caller from the error

# Take the oid/appid from the Forbidden message and resolve it
az ad sp show --id <APPID_OR_OID> --query "{name:displayName, type:servicePrincipalType}" -o table

Confirm the caller is the identity you intended to grant (user vs managed identity vs service principal).

Step 3: Check the authorization grant for that identity

# RBAC mode
VAULT_ID=$(az keyvault show --name <VAULT> --query id -o tsv)
az role assignment list --scope "$VAULT_ID" --assignee <OID> -o table

# Access-policy mode
az keyvault show --name <VAULT> \
  --query "properties.accessPolicies[?objectId=='<OID>'].permissions.secrets" -o jsonc

Empty output means the grant is missing — that is your root cause.

Step 4: Verify the network path

az keyvault network-rule list --name <VAULT> -o jsonc
# Confirm your egress IP is allowed (or the vault uses a private endpoint you can reach)
curl -s https://api.ipify.org

If defaultAction is Deny and your IP/subnet is not listed, fix the network rule before retrying.

Step 5: Grant access and re-test

# RBAC mode
az role assignment create --assignee <OID> \
  --role "Key Vault Secrets User" --scope "$VAULT_ID"

# Access-policy mode
az keyvault set-policy --name <VAULT> --object-id <OID> --secret-permissions get list

# Wait for propagation, then retry
az keyvault secret show --vault-name <VAULT> --name <SECRET> --query value -o tsv

Example Root Cause Analysis

A container app orders-api running on AKS starts failing with Forbidden when reading db-password from prod-kv. The developer insists they granted themselves access to the vault yesterday.

The Forbidden message references a specific identity:

The user, group or application 'appid=8f3b2c1d-...;oid=2c4e6a8b-...' does not have secrets get permission on key vault 'prod-kv;location=eastus'.

Resolving that object ID shows it is not the developer:

az ad sp show --id 2c4e6a8b-1111-2222-3333-444455556666 \
  --query "{name:displayName, type:servicePrincipalType}" -o table
Name                Type
------------------  ---------------
orders-api-mi       ManagedIdentity

The caller is the workload’s managed identity, not the developer’s user account. Checking the vault’s model and the MI’s grant:

az keyvault show --name prod-kv --query "properties.enableRbacAuthorization" -o tsv
VAULT_ID=$(az keyvault show --name prod-kv --query id -o tsv)
az role assignment list --scope "$VAULT_ID" --assignee 2c4e6a8b-1111-2222-3333-444455556666 -o table
true
(empty)

The vault is in RBAC mode and the managed identity has no role assignment — the developer granted access to their own user, which RBAC mode applies to the data plane only if the user reads it interactively. The workload identity was never granted.

Fix: assign the data-plane role to the managed identity and wait for propagation:

az role assignment create --assignee 2c4e6a8b-1111-2222-3333-444455556666 \
  --role "Key Vault Secrets User" --scope "$VAULT_ID"

After role propagation, orders-api reads the secret and starts cleanly.

Prevention Best Practices

  • Standardize on one permission model per environment (prefer RBAC) so you always know whether to debug accessPolicies or az role assignment.
  • Grant access to the identity the workload actually runs as — usually a managed identity — not to a developer’s user account.
  • Always read the appid/oid in the Forbidden message first; it tells you exactly who called and which tenant issued the token.
  • Use data-plane roles (Key Vault Secrets User, Key Vault Administrator) for RBAC vaults — Reader/Contributor grant management-plane access only.
  • Keep vault network rules in config management and document which subnets/IPs each workload egresses from, so firewall denials are obvious.
  • For ad-hoc triage, the free incident assistant can turn a Forbidden body into the likely identity/model gap. See more in Azure guides.

Quick Command Reference

# Which permission model + tenant + network posture?
az keyvault show --name <VAULT> --query "{rbac:properties.enableRbacAuthorization, tenant:properties.tenantId, net:properties.networkAcls.defaultAction}" -o jsonc

# Who is actually calling? (resolve oid/appid from the error)
az ad sp show --id <APPID_OR_OID> --query "{name:displayName, type:servicePrincipalType}" -o table

# RBAC grants on the vault
VAULT_ID=$(az keyvault show --name <VAULT> --query id -o tsv)
az role assignment list --scope "$VAULT_ID" -o table

# Access-policy grants on the vault
az keyvault show --name <VAULT> --query "properties.accessPolicies[].{oid:objectId, secrets:permissions.secrets}" -o table

# Network rules
az keyvault network-rule list --name <VAULT> -o jsonc

# Secret state (disabled / soft-deleted)
az keyvault secret show --vault-name <VAULT> --name <SECRET> --query "attributes.enabled" -o tsv
az keyvault secret list-deleted --vault-name <VAULT> -o table

# Confirm logged-in tenant matches the vault
az account show --query "{tenantId:tenantId, user:user.name}" -o table

# Grant access (pick the model)
az role assignment create --assignee <OID> --role "Key Vault Secrets User" --scope "$VAULT_ID"
az keyvault set-policy --name <VAULT> --object-id <OID> --secret-permissions get list

Conclusion

A Key Vault Forbidden means the caller authenticated but has no authorization to read the secret. The usual root causes:

  1. No access policy granting secrets get (vault in access-policy mode).
  2. No data-plane RBAC role like Key Vault Secrets User (vault in RBAC mode).
  3. The wrong identity was granted — user instead of the managed identity that actually calls.
  4. A firewall or private-endpoint network rule blocks the caller before the data plane.
  5. The secret is disabled or soft-deleted, so the version cannot be read.
  6. You authenticated against the wrong tenant, so the caller’s object ID does not exist in the vault’s directory.

Read the appid/oid/iss in the error first — it names the caller and tenant — then confirm the vault’s model and grant access to that exact identity.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.