Microsoft sent an email this week that caught my attention: all Azure Key Vault API versions prior to 2026-02-01 retire on February 27, 2027. The new API makes Azure RBAC the default access control model for Key Vaults, and legacy access policies become an explicit opt-in. Time to migrate before the deadline forces your hand.
The Change: What Microsoft Is Actually Doing
Starting with API version 2026-02-01 (releasing February 2026):
- New vaults default to RBAC instead of access policies
- Existing vaults keep their current model (no forced migration)
- Old APIs (<2026-02-01) stop working February 27, 2027
This isn’t Microsoft killing access policies entirely, but it is changing the default for new vaults and forcing an API upgrade. If your infrastructure creates vaults programmatically, you need to either migrate to RBAC or explicitly configure access policies in your IaC templates. Of couse, deprecation is the first step towards removal, so I wanted to be proactive and move towards the better-supported model anyways.
Why This Matters
If you do nothing:
- Your SDK/CLI eventually upgrades to API 2026-02-01 (or stops working after Feb 2027)
- Any code that creates new vaults will get RBAC by default
- If your app expects access policies, it gets 403 errors because the permissions don’t exist
- Production breaks in subtle ways that only manifest when new resources are created
The recommended path: Migrate existing vaults to RBAC now, before API changes force emergency fixes.
Inventory: Finding Vaults Still Using Access Policies
First step: identify which vaults need migration. I have many Azure subscriptions across dev/prod environments. This script finds every vault using legacy access policies:
#!/bin/bash
subscriptions=(
"sub-dev-001"
"sub-dev-002"
"sub-prod-001"
"sub-prod-002"
)
echo "Subscription,ResourceGroup,VaultName,AccessPolicyCount,UsingRBAC"
for sub_id in "${subscriptions[@]}"; do
az account set --subscription "$sub_id" 2>/dev/null || continue
vaults=$(az keyvault list --subscription "$sub_id" --query "[].{name:name,rg:resourceGroup}" -o tsv)
while IFS=$'\t' read -r vault_name rg; do
vault_info=$(az keyvault show --name "$vault_name" --resource-group "$rg" \
--query "{accessPolicies:properties.accessPolicies,rbacEnabled:properties.enableRbacAuthorization}" -o json)
policy_count=$(echo "$vault_info" | jq '.accessPolicies | length')
rbac_enabled=$(echo "$vault_info" | jq -r '.rbacEnabled // false')
if [ "$rbac_enabled" = "false" ] && [ "$policy_count" -gt 0 ]; then
echo "$sub_id,$rg,$vault_name,$policy_count,$rbac_enabled"
fi
done <<< "$vaults"
done
Results:
Subscription,ResourceGroup,VaultName,AccessPolicyCount,UsingRBAC
sub-prod-001,app-resources,app-vault,1,false
sub-prod-001,k8s-prod,aks-vault,3,false
sub-prod-001,ai-resources,openai-vault,2,false
sub-prod-002,data-platform,analytics-vault,30,false
Four vaults, 36 total access policies to migrate. One vault (analytics-vault) has 30 principals—likely accumulated over years without cleanup.
The Orphaned Principal Problem
Before migrating, I inspected the actual principals with access:
az keyvault show --name analytics-vault --resource-group data-platform \
--query 'properties.accessPolicies[].[objectId, permissions.keys, permissions.secrets, permissions.certificates]' -o table
Discovery: 13 of the 30 principals returned “UNKNOWN” when queried in Azure AD. These are deleted service principals, users, or groups whose access policies were never cleaned up.
# Resolving ObjectIDs to identities
for oid in "${object_ids[@]}"; do
az ad sp show --id "$oid" 2>/dev/null || \
az ad user show --id "$oid" 2>/dev/null || \
az ad group show --group "$oid" 2>/dev/null || \
echo "$oid,UNKNOWN"
done
Cleanup decision: Remove orphaned principals manually before migration. These don’t represent active access and complicate RBAC mapping.
Access Policies to RBAC Role Mapping
Microsoft provides built-in roles that map to access policy permission sets:
| Access Policy Permissions | RBAC Role |
|---|---|
| Keys: all operations (9+)Secrets: all operations (7+)Certificates: all operations (10+) | Key Vault Administrator |
| Keys: all operations | Key Vault Crypto Officer |
| Secrets: all operations | Key Vault Secrets Officer |
| Secrets: Get, List only | Key Vault Secrets User |
| Keys: Crypto operations only | Key Vault Crypto User |
Examples from my vaults:
Principal: infrastructure-sp
Keys: Get, List, Update, Create, Import, Delete, Recover, Backup, Restore, Decrypt, Encrypt, UnwrapKey, WrapKey, Verify, Sign, Purge, Rotate, GetRotationPolicy, SetRotationPolicy
Secrets: Get, List, Set, Delete, Recover, Backup, Restore, Purge
Certificates: Get, List, Update, Create, Import, Delete, Recover, Backup, Restore, ManageContacts, ManageIssuers, GetIssuers, ListIssuers, SetIssuers, DeleteIssuers, Purge
→ Maps to: Key Vault Administrator
Principal: analytics-etl-sp
Keys: (none)
Secrets: Get, List
Certificates: (none)
→ Maps to: Key Vault Secrets User
The mapping logic in code:
if echo "$keys $secrets $certs" | grep -q '"all"'; then
role="Key Vault Administrator"
elif [[ $(echo "$keys" | jq 'length') -gt 8 ]] && \
[[ $(echo "$secrets" | jq 'length') -gt 5 ]] && \
[[ $(echo "$certs" | jq 'length') -gt 10 ]]; then
role="Key Vault Administrator"
elif [[ $(echo "$keys" | jq 'length') -gt 8 ]]; then
role="Key Vault Crypto Officer"
elif [[ $(echo "$secrets" | jq 'length') -gt 5 ]]; then
role="Key Vault Secrets Officer"
elif echo "$secrets" | grep -q "Get" && echo "$secrets" | grep -q "List"; then
role="Key Vault Secrets User"
fi
Automated Migration Script
I built a migration script that:
- Reads access policies from each vault
- Maps permissions to appropriate RBAC roles
- Creates role assignments
- Enables RBAC on the vault
- Preserves access policies for rollback capability
#!/bin/bash
vaults=(
"sub-prod-001:app-resources:app-vault"
"sub-prod-001:k8s-prod:aks-vault"
"sub-prod-001:ai-resources:openai-vault"
"sub-prod-002:data-platform:analytics-vault"
)
for vault_info in "${vaults[@]}"; do
IFS=':' read -r sub_id rg vault_name <<< "$vault_info"
echo "Processing: $vault_name"
az account set --subscription "$sub_id"
policies=$(az keyvault show --name "$vault_name" --resource-group "$rg" \
--query 'properties.accessPolicies' -o json)
echo "$policies" | jq -r '.[] | @json' | while read -r policy; do
oid=$(echo "$policy" | jq -r '.objectId')
keys=$(echo "$policy" | jq -r '.permissions.keys // []')
secrets=$(echo "$policy" | jq -r '.permissions.secrets // []')
certs=$(echo "$policy" | jq -r '.permissions.certificates // []')
# Verify principal still exists
if az ad sp show --id "$oid" &>/dev/null || \
az ad user show --id "$oid" &>/dev/null || \
az ad group show --group "$oid" &>/dev/null; then
# Map to RBAC role (logic shown above)
# ...
if [ -n "$role" ]; then
echo " Assigning '$role' to $oid"
az role assignment create \
--role "$role" \
--assignee "$oid" \
--scope "/subscriptions/$sub_id/resourceGroups/$rg/providers/Microsoft.KeyVault/vaults/$vault_name"
fi
else
echo " Skipping deleted principal: $oid"
fi
done
# Enable RBAC
az keyvault update --name "$vault_name" --resource-group "$rg" --enable-rbac-authorization true
echo " Migration complete"
done
Key design decision: The script doesn’t delete access policies. Azure preserves them when RBAC is enabled, so if something breaks, rollback is just:
az keyvault update --name vault-name --resource-group rg-name --enable-rbac-authorization false
Access policies reactivate immediately. RBAC role assignments remain but become inactive.
Dry Run First
Before running in production, always dry-run:
# Dry-run version that echoes commands instead of executing
echo "WOULD EXECUTE:"
echo " az role assignment create --role '$role' --assignee '$oid' --scope '...'"
echo " az keyvault update --name $vault_name --enable-rbac-authorization true"
Dry-run output for analytics-vault:
VAULT: analytics-vault
✓ ObjectId: 3c2cf02b (etl-pipeline-sp)
Role: Key Vault Administrator
Keys: 9 operations, Secrets: 7 operations, Certs: 14 operations
✓ ObjectId: 0062b17f (data-engineer-user)
Role: Key Vault Administrator
✓ ObjectId: 59095b28 (youtube-metrics-sp)
Role: Key Vault Secrets User
Keys: 0 operations, Secrets: 2 operations (Get, List)
✗ SKIP - Orphaned Principal: 41da150a
✗ SKIP - Orphaned Principal: 654dff75
WOULD EXECUTE:
az keyvault update --name analytics-vault --enable-rbac-authorization true
Review the output. Verify role mappings make sense. Check that orphaned principals aren’t actually important.
Production Migration Results
=== PRODUCTION MODE ===
VAULT: app-vault
Assigning 'Key Vault Administrator' to b3113d64 (DevOps-Group)
✓ Role assignment created
Enabling RBAC authorization...
✓ Migration complete
VAULT: aks-vault
Assigning 'Key Vault Administrator' to ea881b17 (infrastructure-sp)
✓ Role assignment created
Assigning 'Key Vault Administrator' to a4ce7e4d (platform-engineer)
✓ Role assignment created
Enabling RBAC authorization...
✓ Migration complete
VAULT: openai-vault
Assigning 'Key Vault Administrator' to 5d22ea64 (ai-service-sp)
✓ Role assignment created (already exists from May 2025)
Assigning 'Key Vault Administrator' to c411c37f (ai-developer-sp)
✓ Role assignment created (already exists from May 2025)
Enabling RBAC authorization...
✓ Migration complete
VAULT: analytics-vault
Assigning 'Key Vault Administrator' to 3c2cf02b (etl-pipeline-sp)
✓ Role assignment created
Assigning 'Key Vault Secrets Officer' to 6500ea32 (data-connector-sp)
✓ Role assignment created
[... 16 more assignments ...]
Enabling RBAC authorization...
✓ Migration complete
=== MIGRATION COMPLETE ===
All four vaults migrated successfully. No errors, no 403s, no permission issues.
Verification
Re-run the inventory script to confirm compliance:
bash inventory-access-policies.sh
Output:
Subscription,ResourceGroup,VaultName,AccessPolicyCount,UsingRBAC
(empty - no vaults using legacy access policies)
Perfect. All vaults now use RBAC.
Detailed verification:
az keyvault show --name app-vault --resource-group app-resources \
--query "properties.enableRbacAuthorization"
# Output: true
az role assignment list --scope "/subscriptions/sub-prod-001/resourceGroups/app-resources/providers/Microsoft.KeyVault/vaults/app-vault"
# Output: Shows all RBAC role assignments
What About Terraform/IaC?
If you manage Key Vaults with Terraform, update your azurerm_key_vault resources:
resource "azurerm_key_vault" "example" {
name = "example-vault"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
tenant_id = data.azurerm_client_config.current.tenant_id
sku_name = "standard"
# Enable RBAC authorization
enable_rbac_authorization = true
# Remove access_policy blocks - they're ignored when RBAC is enabled
}
# Replace access policies with role assignments
resource "azurerm_role_assignment" "example" {
scope = azurerm_key_vault.example.id
role_definition_name = "Key Vault Administrator"
principal_id = data.azurerm_client_config.current.object_id
}
Apply this after manually migrating the vault. Terraform will detect enable_rbac_authorization = true already set and not attempt to modify it.
Rollback Plan
If something breaks after migration:
# Disable RBAC (reactivates access policies)
az keyvault update --name vault-name --resource-group rg-name --enable-rbac-authorization false
# Verify access restored
az keyvault secret show --vault-name vault-name --name test-secret
Access policies are preserved during RBAC enablement. Rollback is instant.
When to rollback:
- Applications report 403 errors accessing vault
- Service principals can’t authenticate
- CI/CD pipelines fail with permission errors
When NOT to rollback:
- Role assignments take 5-10 minutes to propagate (wait before assuming failure)
- Deleted principals still appear in access policies (these are already broken, RBAC didn’t cause it)
Timeline and Next Steps
Now (January 2025):
- Inventory vaults using access policies ✓
- Clean up orphaned principals ✓
- Migrate to RBAC ✓
February 2026:
- API version 2026-02-01 releases
- New vaults default to RBAC
- Update SDKs/CLI to use new API
February 27, 2027:
- Old API versions (<2026-02-01) stop working
- Any code still using old APIs breaks
- Access policies still work, but only via new API
Key Takeaways
For SREs and platform engineers:
-
Migrate before the API forces you to. Proactive migration lets you test thoroughly. Reactive migration happens during an outage.
-
Clean up orphaned principals first. Deleted identities clutter access policies and complicate RBAC mapping. Remove them before migration.
-
Access policies aren’t going away entirely. They’re becoming opt-in instead of default. If you have a legitimate reason to keep using them, you can—but you’ll need to explicitly configure them in IaC.
-
Dry-run everything. The stakes are production access to secrets. Verify role mappings before executing.
-
RBAC offers better granularity. Access policies apply at vault level. RBAC supports vault-level, resource group-level, subscription-level, and even individual secret-level permissions.
For anyone managing Azure infrastructure:
This isn’t a breaking change if you handle it proactively. Microsoft is giving 2+ years notice. Use that time to migrate methodically rather than scrambling in February 2027 when your CI/CD pipeline suddenly can’t access Key Vault.
Conclusion
Four vaults migrated, 36 access policies converted to RBAC roles, 13 orphaned principals cleaned up.
The deadline is February 27, 2027, but waiting until 2027 means migrating under pressure. Do it now while you have time to test properly, discover edge cases, and fix issues before they’re production incidents.
When Microsoft sends emails about deprecations, they’re not suggestions—they’re timelines. Treat them as such.