Skip to main content

Azure Key Vault

Volume 4 — Azure Foundations

Key Vault is where secrets, encryption keys, and certificates should live instead of appsettings.json, a connection string hardcoded in a pipeline variable, or an environment variable set by hand in the Portal. This chapter covers the access model and — importantly — a caching behavior that has caused real confusion in Traxs deployments.

1. What Key Vault Stores

Object typeExamples
SecretsConnection strings, API keys, passwords — arbitrary string values
KeysCryptographic keys used for encryption/signing operations, managed without the key material ever leaving the vault
CertificatesTLS certificates, with Key Vault handling the private key material securely alongside the public certificate

For the vast majority of day-to-day Traxs usage, Secrets is the relevant object type — ConnectionStrings--Default, SendGrid API keys, and similar values.

2. Access Policies vs. Azure RBAC

Key Vault historically used its own permission model (access policies), separate from Azure RBAC entirely — and can still be configured either way, which is a common source of confusion when checking "who can access this."

Access Policies (legacy model)Azure RBAC (recommended)
GranularityPer-vault, lists of principals and their allowed operations (get, list, set, delete)Standard Azure role assignments (see the Azure RBAC chapter), scoped to the vault or a resource group
ConsistencyA separate permission system, not visible in az role assignment listUnified with every other Azure resource's permission model
RecommendationLegacy — still supported, but Microsoft recommends migrating to RBACCurrent recommended approach for new vaults

Check which model a given vault is using before troubleshooting accessaz role assignment list will show nothing relevant for a vault still on the legacy access-policy model, which looks identical to "no access granted" until you check the right place.

az keyvault show --name traxs-vault --query "properties.enableRbacAuthorization"
# true = RBAC model, false/null = legacy access policies

3. Managed Identity Access — the Preferred Pattern

The same principle from the Azure Storage chapter applies here even more directly: an App Service accessing Key Vault should authenticate via its own managed identity, not a static credential — since the entire point of Key Vault is removing secrets from configuration, using a secret to access it would defeat the purpose.

# Enable a system-assigned managed identity on the App Service
az webapp identity assign --name roundtrip-api --resource-group traxs-prod

# Grant that identity permission to read secrets
az role assignment create \
--assignee "<app-service-managed-identity-object-id>" \
--role "Key Vault Secrets User" \
--scope "/subscriptions/<sub-id>/resourceGroups/traxs-prod/providers/Microsoft.KeyVault/vaults/traxs-vault"

4. Key Vault References in App Service Settings

Rather than an application reading secrets from Key Vault at runtime via SDK calls, App Service supports Key Vault references directly in application settings — the setting's value is a pointer to a Key Vault secret, and App Service resolves it transparently before the app ever sees it.

ConnectionStrings__Default = @Microsoft.KeyVault(SecretUri=https://traxs-vault.vault.azure.net/secrets/ConnectionStrings--Default/)

From the application's perspective, ConnectionStrings__Default is just a normal environment variable with the actual resolved value — the app code has no idea (and needs no code) to know it came from Key Vault at all. This requires the App Service's managed identity to have Key Vault Secrets User access, as set up in Section 3.

5. The Secret Caching Gotcha

This is the detail that has caused real confusion in Traxs deployments, and it's worth internalizing precisely:

App Service caches resolved Key Vault reference values. Updating a secret's value in Key Vault does not automatically propagate to an App Service already running with the old cached value.

Symptom: rotated a secret in Key Vault → App Service still behaves as if it has the old value
Expected fix that DOESN'T work: az webapp restart
Actual fix required: fully STOP then START the App Service (not just restart)
az webapp stop --name roundtrip-api --resource-group traxs-prod
az webapp start --name roundtrip-api --resource-group traxs-prod

A plain az webapp restart recycles the application process but does not reliably force App Service to re-fetch Key Vault reference values from scratch — only a full stop/start cycle reliably does. This is easy to miss because a restart looks like it should be equivalent, and in almost every other configuration-change scenario, it is — this is a specific, documented exception for Key Vault reference caching.

6. Secret Versioning

Key Vault keeps every previous version of a secret when it's updated, rather than overwriting in place:

az keyvault secret set --vault-name traxs-vault --name ApiKey --value "new-value"
az keyvault secret list-versions --vault-name traxs-vault --name ApiKey --output table

A Key Vault reference without a specific version pinned (as in Section 4's example, using just the secret name) always resolves to the current version — which is almost always what you want for application configuration, since it means a secret rotation doesn't require updating the reference itself, just the underlying value (subject to the caching behavior in Section 5).

7. Practical Rotation Workflow

# 1. Set the new secret value (creates a new version, old version still retained)
az keyvault secret set --vault-name traxs-vault --name ConnectionStrings--Default --value "new-connection-string"

# 2. Force App Service to actually pick it up
az webapp stop --name roundtrip-api --resource-group traxs-prod
az webapp start --name roundtrip-api --resource-group traxs-prod

# 3. Verify
az webapp log tail --name roundtrip-api --resource-group traxs-prod

8. Troubleshooting Playbook

SymptomLikely causeWhat to check
Rotated a secret, App Service still behaves like the old valueKey Vault reference caching, restart alone doesn't clear itFull az webapp stop then az webapp start, not just restart
App Service can't resolve a Key Vault reference at all — shows as unresolved/error in the PortalManaged identity not granted access, or wrong access model (RBAC vs. legacy policy) checkedConfirm which access model the vault uses (Section 2); grant the correct type of permission accordingly
403 Forbidden accessing a secret via CLI, but the Portal shows you as an Owner on the resource groupVault is using the legacy access-policy model, and Owner-level Azure RBAC doesn't automatically grant vault data access under that modelCheck enableRbacAuthorization; add an explicit access policy if the vault is still on the legacy model
Secret works for one App Service, not an identical oneEach App Service has its own distinct managed identity — a role/policy granted to one doesn't apply to anotherConfirm access was granted to the correct App Service's specific managed identity object ID
Local development can't reach Key Vault at allLocal developer identity not granted access, or no az login session active locallyaz login locally, and confirm the developer's own identity (not just the App Service's) has appropriate access if local dev reads from Key Vault directly

9. Quick Reference

CategoryItemDetail
ObjectsSecrets / Keys / CertificatesWhat Key Vault stores
Access modelRBAC (recommended) vs. legacy access policiesCheck enableRbacAuthorization before troubleshooting access
Preferred accessManaged identity + Key Vault Secrets User roleNo static credential needed
App ServiceKey Vault reference@Microsoft.KeyVault(SecretUri=...) in an app setting
Critical gotchaSecret rotation cachingFull stop/start required — restart is not sufficient
Commandaz keyvault secret setCreate a new version of a secret
Commandaz webapp stop / az webapp startForce Key Vault reference re-resolution

Part of the Traxs Engineering Handbook — Volume 4: Azure Foundations. Companion chapters in this volume: Azure CLI, Azure RBAC, Azure Networking, Azure Storage, Microsoft Entra ID.