Azure Key Vault — Secrets, Keys, and Certificates in .NET
Use Azure Key Vault in .NET applications: storing secrets, injecting Key Vault into IConfiguration, Managed Identity authentication, key rotation, and certificate management for clinical APIs.
Why Key Vault
Without Key Vault:
appsettings.Production.json:
"ExternalApi": { "ApiKey": "sk-live-abc123..." }
Problems:
→ Secret is in source control — every developer who cloned the repo has it
→ Rotating the secret requires a code deployment
→ No audit trail of who read the secret or when
→ Secret lives in plain text on the server's filesystem
With Key Vault:
appsettings.json:
"ExternalApi": { "ApiKey": "@Microsoft.KeyVault(SecretUri=https://clinical-kv.vault.azure.net/secrets/ExternalApiKey)" }
or, via AddAzureKeyVault: key vault secrets appear directly in IConfiguration
Benefits:
→ Secret never stored in source control
→ Rotation: update in Key Vault, application picks it up without redeployment
→ Full audit log: who read which secret and when
→ RBAC: developer Bob can write secrets, production App Service can only read themIntegrating Key Vault with IConfiguration
// Program.cs — add Key Vault as a configuration source
// Secrets become available via IConfiguration like any other setting
var keyVaultUri = new Uri(builder.Configuration["AzureKeyVault:Uri"]!);
builder.Configuration.AddAzureKeyVault(
keyVaultUri,
new DefaultAzureCredential());
// DefaultAzureCredential:
// → In production (App Service): uses Managed Identity automatically
// → In local development: uses Azure CLI credentials (az login)
// → In CI/CD: uses environment variables (AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID)
// Now read secrets the same way as any configuration value:
var apiKey = builder.Configuration["ExternalApi--ApiKey"];
// Key Vault secret name: "ExternalApi--ApiKey" (double-dash = colon in IConfiguration)
// This maps to: IConfiguration["ExternalApi:ApiKey"]Secret Naming Convention
Key Vault secret names must match .NET IConfiguration key paths.
IConfiguration uses ":" as the separator: "ConnectionStrings:Clinical"
Key Vault does not allow ":" in secret names — use "--" (double dash) instead.
IConfiguration key → Key Vault secret name
ConnectionStrings:Clinical → ConnectionStrings--Clinical
ExternalApi:ApiKey → ExternalApi--ApiKey
Jwt:SigningKey → Jwt--SigningKey
Fhir:BaseUrl → Fhir--BaseUrl
Smtp:Password → Smtp--Password
When AddAzureKeyVault is registered, it automatically maps "--" back to ":"
so builder.Configuration["ConnectionStrings:Clinical"] works as expected.Setting Secrets with Azure CLI
# Create or update a secret
az keyvault secret set \
--vault-name clinical-kv \
--name "ConnectionStrings--Clinical" \
--value "Server=clinical-sql.database.windows.net;Initial Catalog=ClinicalDb;Authentication=Active Directory Managed Identity"
# Read a secret (for verification)
az keyvault secret show \
--vault-name clinical-kv \
--name "ConnectionStrings--Clinical" \
--query "value" -o tsv
# List all secrets (shows names only, not values)
az keyvault secret list --vault-name clinical-kv --query "[].name" -o table
# Grant App Service read access to Key Vault
az keyvault set-policy \
--name clinical-kv \
--object-id $(az webapp identity show --name clinical-platform --resource-group clinical-rg --query principalId -o tsv) \
--secret-permissions get listAccessing Secrets Programmatically
// Sometimes you need to access a secret directly (e.g., for rotation checks)
// rather than via IConfiguration injection
public sealed class ExternalApiClient
{
private readonly SecretClient _secretClient;
private readonly HttpClient _httpClient;
public ExternalApiClient(SecretClient secretClient, HttpClient httpClient)
{
_secretClient = secretClient;
_httpClient = httpClient;
}
public async Task<string> GetMedicationDataAsync(string medicationCode, CancellationToken ct)
{
// Fetch the API key fresh each time (supports key rotation without restart)
var secret = await _secretClient.GetSecretAsync("ExternalApi--ApiKey", null, ct);
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", secret.Value.Value);
var response = await _httpClient.GetAsync($"/medications/{medicationCode}", ct);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(ct);
}
}
// DI registration:
builder.Services.AddSingleton(_ =>
new SecretClient(
new Uri("https://clinical-kv.vault.azure.net/"),
new DefaultAzureCredential()));Certificates in Key Vault
// Store TLS certificates in Key Vault — automatic renewal with App Service
// App Service → TLS/SSL settings → Import certificate from Key Vault
// The certificate is rotated in Key Vault and App Service picks it up automatically
// For custom certificate loading (e.g., for mTLS to downstream services):
public static X509Certificate2 LoadCertificate(CertificateClient certClient, string name)
{
var certWithPolicy = certClient.GetCertificate(name);
var secretClient = new SecretClient(
new Uri("https://clinical-kv.vault.azure.net/"), new DefaultAzureCredential());
// Certificate is stored as a secret (PFX format) in Key Vault
var secret = secretClient.GetSecret(certWithPolicy.Value.SecretId.AbsolutePath.TrimStart('/'));
var pfxBytes = Convert.FromBase64String(secret.Value.Value);
return new X509Certificate2(pfxBytes);
}Production issue I've seen: A team stored 14 secrets across 3 environments in
appsettings.{Environment}.jsonfiles. These were committed to the repository with access controls, but a junior developer accidentally included them in a PR that was temporarily opened as a public repository for a portfolio demo. All 14 secrets were exposed within 3 minutes (automated scanners). Rotation required contacting 5 external vendors (FHIR API, SMS gateway, payment processor, email service, analytics) during an out-of-hours incident call. The rotation took 11 hours. Migrating all secrets to Key Vault before go-live would have meant the repository contained no secrets to expose.
Key Takeaway
Never store secrets in source control or configuration files. Add Azure Key Vault as an
IConfigurationsource viaAddAzureKeyVault— secrets become available transparently viaIConfigurationwith no code changes. Use Managed Identity (DefaultAzureCredential) — no client secrets needed to access Key Vault. Use the--double-dash naming convention to map Key Vault secret names to IConfiguration paths. Grant the App Service Managed Identity onlygetandlistpermissions on secrets — notsetordelete.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.