Resources / Field guide
Azure Key Vault with App Services: secrets without secrets
How to get an ASP.NET Core app reading its secrets from Azure Key Vault with a managed identity — no credentials stored anywhere — and the two RBAC traps that turn a five-minute setup into an afternoon of 500.30s.
The problem with secrets in config
Every app has secrets — a client secret, a connection string, an API key. The path of least resistance is to drop them in appsettings.json and move on. The trouble shows up later: a secret committed to git is leaked the moment it lands in history, even in a private repo, and the only real fix is to rotate it. You want secrets that live in exactly one place, that no human can read by accident, and that you can rotate without a redeploy.
Azure Key Vault plus a managed identity gives you that. The app authenticates to the vault using an identity Azure manages for it, so there's no bootstrap credential to store — you've solved the "how do I secure the thing that secures the secrets" problem by not having one. Here's the whole setup, including the parts the docs gloss over.
How it fits together
Step 1 — Wire Key Vault into configuration
In Program.cs, add the vault as a configuration source when a vault URI is present. Gating it on config means local runs that don't set the URI just skip it:
var builder = WebApplication.CreateBuilder(args);
var keyVaultUri = builder.Configuration["KeyVault:Uri"];
if (!string.IsNullOrWhiteSpace(keyVaultUri))
{
builder.Configuration.AddAzureKeyVault(
new Uri(keyVaultUri),
new DefaultAzureCredential());
}
DefaultAzureCredential is the quiet hero here: on the App Service it uses the managed identity automatically, and on your laptop it falls back to whatever you're signed in as. Same code, both environments, zero stored credentials. You'll need Azure.Identity and Azure.Extensions.AspNetCore.Configuration.Secrets from NuGet.
Step 2 — Name your secrets so .NET maps them
Key Vault secret names can't contain a colon, but .NET config keys are colon-delimited. The provider bridges this by translating a double dash into a colon. So a secret named AzureAd--ClientSecret in the vault becomes AzureAd:ClientSecret in configuration, and your code reads it with no special handling:
// Vault secret: AzureAd--ClientSecret
// Reads back as: AzureAd:ClientSecret
var clientSecret = builder.Configuration["AzureAd:ClientSecret"];
Create the secrets once from the CLI (or the portal):
az keyvault secret set --vault-name <your-vault> --name "AzureAd--ClientSecret" --value "<secret>"
az keyvault secret set --vault-name <your-vault> --name "AzureAd--ClientId" --value "<id>"
Step 3 — Turn on the managed identity and grant it access
Give the App Service a system-assigned identity, then grant that identity read access to the vault's secrets:
# Turn on the system-assigned managed identity
az webapp identity assign --name <app> --resource-group <rg>
# Grant the identity read access to secrets (data-plane role)
az role assignment create \
--assignee <identity-principal-id> \
--role "Key Vault Secrets User" \
--scope "/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.KeyVault/vaults/<your-vault>"
Trap #1 — Owner/Contributor does not let you read secrets. Key Vault has two RBAC planes. Management-plane roles (Owner, Contributor, Reader) let you manage the vault resource but grant zero access to the secrets inside it. Reading secrets is a data-plane permission. The app's identity needs "Key Vault Secrets User"; a human who needs to manage secrets needs "Key Vault Administrator". Assigning "Reader" and expecting it to work is the single most common reason this setup fails.
When it breaks: reading the 500.30
If the vault call fails, it fails at startup — AddAzureKeyVault runs before the app is built — so the whole process dies and IIS serves a bare HTTP 500.30 (ANCM In-Process Start Failure) with no useful detail in the browser. The real exception is hiding in the platform logs:
# Kudu / SCM console:
LogFiles/eventlog.xml ← the actual startup exception lives here
# or stream it:
az webapp log tail --name <app> --resource-group <rg>
Nine times out of ten the exception is an access-denied from the vault, which sends you right back to Trap #1.
Trap #2 — RBAC propagation is not instant. A new role assignment can take a few minutes to take effect. If you assign "Key Vault Secrets User" and immediately restart the app, it can still fail with access denied — then succeed on its own a few minutes later once propagation catches up. Give it five minutes before you start doubting the role itself.
Rotating a leaked secret
The reason this whole exercise pays off: rotation becomes trivial and safe. If a secret ever leaks — say an old client secret is sitting in git history — you regenerate it at the source (for an Entra ID app registration, that's a new client secret), store the new value only in the vault, and you're done. No code change, no appsettings.json edit, no redeploy. Treat anything that ever touched source control as already compromised and rotate it on principle.
When this approach fits
Use Key Vault with a managed identity for anything that runs in Azure and holds a secret you'd be unhappy to see in a screenshot — which is most production apps. It's mild overkill for a throwaway demo, and exactly right the moment real credentials are involved. The setup is genuinely a few minutes once you know the two traps; budget your time for the RBAC plane, not the code.
Deploying .NET to Azure and want it done right?
I build and ship production ASP.NET Core apps on Azure — identity, secrets, and the deployment plumbing included. If you want a senior developer on it directly, let's talk.
Work with me