Azure Key Vault: Access Policies vs RBAC

Azure Key Vault: Access Policies vs RBAC - article cover image Cloud Security
Time it takes to read this article 7 minutes.

Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.

Introduction

Azure Key Vault centralizes the storage of secrets, encryption keys, and TLS certificates that applications need at runtime, which makes it one of the highest-value targets in an Azure tenant — a single over-permissioned principal on a vault can mean database connection strings, storage account keys, and signing certificates for an entire environment. Authorization to a vault’s data plane is controlled by one of two mutually exclusive models: the legacy vault Access Policy model, or the newer Azure RBAC model. Which one is active is a single boolean on the vault, enableRbacAuthorization, and that switch determines everything about how permissions are granted, scoped, and audited.

The two models are not just different syntax for the same thing — they have materially different security properties. Access Policies are coarse, vault-wide, and invisible to Azure AD’s role-assignment tooling; Azure RBAC is fine-grained, integrates with Privileged Identity Management (PIM) and Conditional Access, and is auditable through the same az role assignment surface used for every other Azure resource. Environments that migrated from classic vaults, or that were built by teams unaware of the RBAC option, routinely carry forward Access Policy grants that are far broader than intended — and because Access Policies don’t show up in the standard IAM blade reviews teams run for RBAC, that over-permissioning tends to survive audits indefinitely.

Attack Prerequisites

Exploiting Key Vault authorization is about finding and using an identity that already has more data-plane access than it needs. For this to be viable:

  • Some foothold identity — a compromised user, a leaked service principal client secret/certificate, a managed identity accessible via SSRF or code execution on a resource, or a CI/CD pipeline credential.
  • That identity (or a group it belongs to) holds broad vault permissions — either via an over-scoped Access Policy or an RBAC role like Key Vault Secrets Officer/Key Vault Administrator assigned at too high a scope.
  • Network access to the vault’s data-plane endpoint — trivial if the vault firewall is set to allow public access from all networks, which is still the default for many vaults.
  • No compensating control such as Conditional Access, PIM just-in-time activation, or Private Link forcing traffic through a specific VNet.

How It Works

The Access Policy model grants permissions per security principal, per plane — keys, secrets, and certificates each get their own list of allowed operations (get, list, set, delete, backup, restore, purge, recover, and more for keys). Critically, an Access Policy applies to the *entire vault*: there is no way to scope a policy to a single secret or certificate. If a principal is granted get/list on secrets, it can read every secret in that vault, present and future. Access Policies also have no concept of deny assignments, no PIM eligibility/activation workflow, and no native Conditional Access enforcement at the data-plane call — a stolen credential with a valid token can call the vault directly.

Azure RBAC authorization instead treats Key Vault data-plane operations as ordinary Azure resource actions, governed by built-in roles such as Key Vault Secrets User (read secret values), Key Vault Secrets Officer (manage secrets), Key Vault Certificates Officer, Key Vault Crypto User/Officer, Key Vault Administrator (full data-plane control), and Key Vault Reader (metadata only, no secret values). Role assignments made with az role assignment create can be scoped at the management group, subscription, resource group, vault, or — because Key Vault supports sub-resource RBAC scoping — even at an individual key, secret, or certificate object. Because these are standard Azure AD role assignments, they participate in PIM eligible/active assignment, Conditional Access policies (e.g. requiring MFA or a compliant device to activate), and appear in every az role assignment list or Azure AD Access Reviews sweep.

The security gap in practice is migration debt. enableRbacAuthorization is set once on a vault; setting it to true does not delete existing Access Policies, but they simply stop being evaluated. Teams that flip the switch later without cleaning up old policies leave stale documentation behind, and teams that never flip it keep relying on all-or-nothing Access Policy grants that a standard Azure RBAC/IAM audit will never surface, because those policies live in properties.accessPolicies on the vault object, not in the tenant’s role-assignment graph.

Vulnerable Code / Configuration

A vault still on the legacy model, with a broad Access Policy granted to an Azure AD group (which may contain far more members than the vault owner realizes) and left network-open:

# Vault created without RBAC authorization (legacy Access Policy model)
az keyvault create \
  --name kv-prod-payments \
  --resource-group rg-payments \
  --enable-rbac-authorization false \
  --public-network-access Enabled

# Over-broad policy: entire 'DevTeam-All' AD group gets full secret,
# key, and certificate management on every object in the vault.
az keyvault set-policy \
  --name kv-prod-payments \
  --object-id <DevTeam-All-group-object-id> \
  --secret-permissions get list set delete backup restore purge recover \
  --key-permissions get list create delete sign verify backup restore purge \
  --certificate-permissions get list create delete managecontacts
Bash

The equivalent ARM/JSON access policy embedded in a vault template shows the same problem — one entry, no per-secret scoping, applying to the whole vault:

{
  "tenantId": "<tenant-id>",
  "objectId": "<DevTeam-All-group-object-id>",
  "permissions": {
    "secrets": ["get", "list", "set", "delete", "backup", "restore", "purge"],
    "keys": ["get", "list", "create", "delete", "sign", "backup", "restore"],
    "certificates": ["get", "list", "create", "delete", "managecontacts"]
  }
}
JSON

Compounding this, the vault’s network rule set allows public access from any network, so any principal that authenticates to Azure AD and holds a valid token for https://vault.azure.net can reach the data plane directly, with no requirement to be on a corporate network or a peered VNet.

Walkthrough / Exploitation

With a compromised service principal or user token in hand, the first step is discovering which vaults it can reach and what it’s actually allowed to do. Access Policies are visible directly on the vault object:

az login --service-principal -u <client-id> -p <client-secret> --tenant <tenant-id>

az keyvault list --query "[].{name:name, rg:resourceGroup}" -o table
az keyvault show --name kv-prod-payments \
  --query "properties.{rbac:enableRbacAuthorization, policies:accessPolicies, network:networkAcls}"
Bash

If enableRbacAuthorization is false and the caller’s object ID (or a group it belongs to) appears in accessPolicies with get/list on secrets, the data plane is open. Enumerate and dump everything the policy allows:

az keyvault secret list --vault-name kv-prod-payments -o table
az keyvault secret show --vault-name kv-prod-payments --name sql-connstring \
  --query value -o tsv

az keyvault certificate list --vault-name kv-prod-payments -o table
az keyvault key list --vault-name kv-prod-payments -o table
Bash

Harvested secrets are rarely the end goal in themselves — they’re pivot material. A SQL connection string yields direct database access; a second service principal’s client secret stored as a Key Vault secret (a common pattern for app-to-app auth) lets the attacker az login as that identity and repeat the enumeration against whatever it has access to, walking laterally across subscriptions. Tools such as AzureHound (BloodHound’s Azure collector) and the Az.KeyVault PowerShell module (Get-AzKeyVaultSecret, Get-AzKeyVault) are commonly used to enumerate vaults and access policies at scale across a tenant rather than one vault at a time.

Note: A vault can be RBAC-enabled and still be over-permissioned — assigning Key Vault Administrator or Key Vault Secrets Officer at the subscription scope instead of the individual vault is functionally identical to a broad Access Policy, just expressed differently. Migrating models fixes the auditability problem, not the least-privilege problem, unless role scopes are also tightened.

Opsec: az keyvault secret show calls are logged individually to the vault’s AuditEvent diagnostic category when logging is enabled, including the caller’s object ID, IP address, and the specific secret name accessed — a burst of SecretGet calls across many secret names from one principal in a short window is a strong signal, so operators should expect this activity to be recoverable in an incident response timeline even if the vault owner never notices in real time.

Detection and Defense

The durable fix is moving off the Access Policy model entirely and treating vault access like any other Azure RBAC surface, with tight scoping:

  • Migrate to RBAC authorization — set enableRbacAuthorization: true and replace Access Policy entries with least-privilege built-in roles (Key Vault Secrets User for read-only apps, not Secrets Officer or Administrator).
  • Scope role assignments narrowly — assign at the individual vault or object level rather than subscription/resource-group scope, and use PIM eligible assignments plus Conditional Access for any human access to Administrator– or Officer-level roles.
  • Restrict network access — move from ‘Allow public access from all networks’ to selected virtual networks/IP ranges or Private Link, using az keyvault network-rule add / --default-action Deny.
  • Enable diagnostic logging — send the AuditEvent category to a Log Analytics workspace so every SecretGet, SecretList, KeyGet, and CertificateGet call is queryable, and enable Microsoft Defender for Key Vault for anomaly alerting.
  • Keep soft-delete and purge protection enabled (defaults on new vaults) so deleted secrets/keys can’t be permanently destroyed to cover tracks.
  • Periodically audit properties.accessPolicies on every vault directly — it will never appear in a standard Azure AD/RBAC role-assignment review.

Real-World Impact

Key Vault over-permissioning is one of the most common findings in Azure cloud security assessments, precisely because Access Policies are easy to grant broadly during initial setup and rarely revisited afterward, and because many organizations still run vaults created years before RBAC authorization was generally available. Since vaults frequently hold the credentials that connect applications to databases, storage, and other Azure services, a single over-permissioned identity reaching a production vault is a routine path from an initial low-privilege foothold to broad lateral movement across a subscription.

Conclusion

Access Policies and Azure RBAC solve the same problem — who can read and manage vault contents — with very different blast radii and auditability. Legacy Access Policies grant vault-wide, plane-wide access that is invisible to standard RBAC reviews, while RBAC roles are scopable, PIM- and Conditional-Access-aware, and show up in the same audit trail as every other Azure permission. Migrating to enableRbacAuthorization: true, assigning narrowly scoped built-in roles, restricting network access, and logging every data-plane call closes off one of the quieter, high-value paths into an Azure environment.

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL