Azure Key Vault & Managed Identity
Zero-secret configuration β how to get secrets into your app without storing credentials anywhere.
Azure Key Vault stores secrets, certificates, and cryptographic keys in a hardware-backed, audited service. Managed Identity gives your Azure resources a cryptographic identity backed by Azure AD β eliminating the bootstrap credential problem. Combined, they enable zero-secret configuration: no passwords in code, config files, environment variables, or CI/CD pipelines.
The classic credential problem: you need a secret to get a secret. Managed Identity solves this β Azure assigns a cryptographic identity to your resource (App Service, VM, Function) backed by Azure AD. No password, no key file, no rotation needed.
At runtime, the Azure SDK calls http://169.254.169.254/metadata/identity/oauth2/token β an Azure-internal endpoint only reachable from within Azure. This returns a short-lived JWT signed by Azure AD. DefaultAzureCredential handles this automatically.
Key Vault receives the request with the Bearer token. It validates the signature against Azure AD and then checks: does this identity have Key Vault Secrets User RBAC role (or access policy permission) on this vault? If not, 403 Forbidden.
The secret value is returned over TLS. Key Vault enforces a rate limit of 5 requests per second per secret. Your code should cache the value (5 minutes is typical) β refresh too rarely and you pick up rotated secrets too late; refresh too often and you hit rate limits.
Key Vault stores all versions of a secret. When you set a new value, the previous version is not deleted β it's retained with a 90-day soft-delete window. Apps that fetch the latest version automatically get the new value on the next cache refresh. No redeployment needed.
Key Concepts
Azure SDK client for reading/writing Key Vault secrets. Pair with DefaultAzureCredential for zero-config auth that works in dev (Azure CLI) and prod (Managed Identity) without code changes.
Credential chain that tries multiple auth sources in order: environment variables β workload identity β managed identity β Visual Studio β Azure CLI. Same code works everywhere.
System-assigned: 1-to-1 with resource, deleted with resource. User-assigned: independent lifecycle, shareable across resources. Both eliminate the need to store any credentials.
App Service / Functions config value syntax: @Microsoft.KeyVault(SecretUri=...). Azure resolves this using the resource's managed identity at runtime. Zero SDK code required.
Soft-delete retains deleted secrets for 7β90 days (recoverable). Purge protection prevents immediate permanent deletion, requiring the retention period to expire first. Both must be enabled to prevent accidental loss.
RBAC is the modern model β uses Azure AD roles (Key Vault Secrets User/Officer/Admin), supports Conditional Access and PIM. Access Policies is legacy, vault-level only, retiring. Always use RBAC for new vaults.
Every SetSecret() call creates a new version. The 'current' version is the latest active one. Applications fetching by name (without version) always get the latest, enabling zero-downtime rotation.
1// .NET 8 β Zero-secret config with Key Vault + Managed Identity2// Program.cs3var builder = WebApplication.CreateBuilder(args);45// DefaultAzureCredential tries, in order:6// 1. Environment variables, 2. Workload Identity, 3. Managed Identity,7// 4. Visual Studio, 5. Azure CLI, 6. Azure PowerShell, 7. Interactive8var credential = new DefaultAzureCredential();910// Add Key Vault as a configuration provider11builder.Configuration.AddAzureKeyVault(12 new Uri(builder.Configuration["KeyVault:VaultUri"]!),13 credential14);1516// SecretClient for imperative access17builder.Services.AddSingleton(_ =>18 new SecretClient(new Uri(builder.Configuration["KeyVault:VaultUri"]!), credential));1920var app = builder.Build();2122// -----------------------------------------------23// Fetching a secret imperatively (with caching)24// -----------------------------------------------25public class SecretService26{27 private readonly SecretClient _client;28 private readonly IMemoryCache _cache;2930 public async Task<string> GetSecretAsync(string name)31 {32 // Cache for 5 minutes β balance freshness vs rate limits (5 RPS per secret)33 return await _cache.GetOrCreateAsync($"secret:{name}", async entry =>34 {35 entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);36 var secret = await _client.GetSecretAsync(name);37 return secret.Value.Value;38 });39 }40}4142// -----------------------------------------------43// App Service config reference (NO code needed!)44// -----------------------------------------------45// In App Service Application Settings, add:46// Name: ConnectionStrings__Sql47// Value: @Microsoft.KeyVault(SecretUri=https://contoso-prod.vault.azure.net/secrets/sql-connstr/)48//49// App Service resolves this at runtime using its managed identity.50// The secret value appears as a regular config value to your app.
Every secret stored in a config file, environment variable, or CI/CD pipeline is a credential leak waiting to happen β via git history, log files, process dumps, or stolen CI tokens. Managed Identity + Key Vault eliminates all of these attack surfaces. The app never possesses a long-lived credential; it requests short-lived tokens that expire and rotate automatically.
Common Pitfalls
1Accidental Secret Deletion Without Purge Protection
A developer ran a cleanup script to remove unused Key Vault secrets. A production secret named db-connection was accidentally deleted. The app started throwing connection errors within seconds.
The vault had soft-delete disabled (pre-2021 default). The deletion was permanent and immediate. The secret was gone with no recovery path. The team had to manually reconstruct the connection string from a colleague's local notes.
Enabled soft-delete (now mandatory on all new vaults) and purge protection on all vaults. Implemented a policy via Azure Policy that denies Key Vault creation without purge protection. Added RBAC so developers have Key Vault Secrets User (read-only) and only the pipeline service principal has Key Vault Secrets Officer.
Takeaway: Soft-delete is now mandatory on new Azure Key Vaults, but legacy vaults may still lack it. Purge protection is a separate setting and must be explicitly enabled. Without it, a single az keyvault secret delete command causes permanent data loss.
2Key Vault Firewall Blocking App Service After IP Change
A team enabled the Key Vault firewall to restrict access to known IPs. The app worked fine until App Service automatically scaled out, assigning new outbound IPs that weren't in the allow-list. The app started returning 403 on every secret fetch.
App Service outbound IPs are not static β they change when you scale out, change pricing tier, or App Service rebalances its infrastructure. A firewall rule based on the 4 'main' IPs misses the additional IPs used during scale-out events.
Replaced IP firewall rules with private endpoints (Key Vault Private Link). The Key Vault is only accessible via the VNet, and the App Service connects through VNet Integration. Alternatively, added all possible outbound IPs from the App Service properties page and set up an alert when IPs change.
Takeaway: App Service has multiple possible outbound IPs (show in portal under 'Outbound IP addresses'). Using IP firewall on Key Vault is fragile β use private endpoints + VNet Integration for production environments.
3User-Assigned Managed Identity Not Attached to App Service
A team created a user-assigned managed identity, granted it Key Vault Secrets User RBAC, and updated the app code to use DefaultAzureCredential. Local dev worked (Azure CLI fallback). Deploying to App Service returned 403 on every Key Vault request.
Creating a user-assigned managed identity and granting it RBAC is insufficient. The identity must also be explicitly assigned to the App Service resource in the Identity blade under User-assigned. Without this step, the App Service has no identity to present to IMDS.
Assigned the user-assigned identity to the App Service via the Identity > User assigned > + Add flow. Added this step to the Terraform / Bicep deployment template to prevent recurrence. Added an integration test that calls Key Vault on startup and fails the health check if access is denied.
Takeaway: User-assigned identity requires two steps: (1) grant RBAC on Key Vault, (2) attach to the resource. Most teams complete step 1 and forget step 2. System-assigned identity is simpler for single-resource scenarios β it's created and attached atomically.