Zero-config secrets: Key Vault + Managed Identity
appsettings.json ends up in Git history, hence compromised. The idiomatic remedy on Azure: Key Vault to store secrets, Managed Identity to access them without a password. Once both are wired up, neither your configuration nor your code contains a single sensitive string.
The secret used to read secrets
IDENTITY_ENDPOINT variables on App Service and Container Apps, the IMDS 169.254.169.254 on a VM) that the SDK queries. Nothing sensitive touches the code or the config.
DefaultAzureCredential chains several authentication sources and keeps the first one that responds. In the cloud, it's the managed identity; locally, a developer tool. The same binary works on both sides.
GetSecretAsync returns a Response<KeyVaultSecret> : secret.Value carries the string, secret.Properties carries the metadata (version, expiration date, enabled state). Each call hits the vault, a point I come back to further down.
System-assigned or user-assigned
DefaultAzureCredential doesn't know which one to use. You need to point it to the clientId via DefaultAzureCredentialOptions { ManagedIdentityClientId = "..." } .
Key Vault as a configuration provider
SecretClient by hand everywhere, wire Key Vault directly into the ASP.NET Core configuration system. All secrets become ordinary configuration entries, merged with appsettings.json and environment variables.
AddAzureKeyVault lives in the Azure.Extensions.AspNetCore.Configuration.Secrets package. The naming convention matters: Key Vault forbids : in a secret name, so you write -- , which the default KeyVaultSecretManager translates back into a section separator. Db--ConnectionString becomes Db:ConnectionString , exactly like the rest of your typed config bound via IOptions<T> .
RBAC rather than access policies
enableRbacAuthorization property). Prefer Azure RBAC : it uses the same roles and the same audit trail as the rest of your resources, whereas the legacy access policies live off in an isolated corner of the vault.
$PRINCIPAL_ID is the objectId of the managed identity, returned when it's enabled on the resource. Least privilege shows up in the scope as much as in the role: scope the assignment to a single vault, not to the whole resource group.
Rotation and caching
AzureKeyVaultConfigurationOptions.ReloadInterval : pass it a TimeSpan and it re-fetches the secrets at a regular interval. This is polling , not push: Key Vault doesn't notify the app of a change, the app queries it. An interval of a few minutes is a reasonable trade-off between freshness and call volume.
GetSecret hits the vault, which applies throttling (on the order of a few thousand transactions per ten-second window, across all secrets). Reading a secret on every HTTP request is an anti-pattern: load it at startup, or cache it with a lifetime, and let ReloadInterval handle the rotation. SecretClient doesn't cache anything for you.
Local dev and the cloud, the same code
DefaultAzureCredential fetches the token from the managed identity. On your machine, it walks down the chain until it finds a developer session: the Azure CLI ( az login ), Visual Studio, or the Azure Developer CLI. No variable or secret file to manage locally.
DefaultAzureCredentialOptions , which also speeds up startup.
Key Vault and Managed Identity remove secrets from code and config: they live in the vault, access goes through a platform identity, and rotation doesn't require a redeployment. There's nothing left to protect in the repo.