The second part of the series about Azure Key Vault. In the first part we integrated Key Vault with IConfiguration. Here we deal with secret rotation and certificates.
Automatic secret rotation
Manual secret rotation is a fragile process: someone has to generate the new value, update it in Key Vault, propagate it to all systems using it, and ensure there is no downtime during the transition. Key Vault provides mechanisms to automate and simplify this process.
Expiration policies
# Set an expiration date on a secret
az keyvault secret set-attributes \
--vault-name my-app-keyvault \
--name "Stripe--ApiKey" \
--expires "2026-01-01T00:00:00Z"
# Tag for notification 30 days before expiration
az keyvault secret set-attributes \
--vault-name my-app-keyvault \
--name "Stripe--ApiKey" \
--tags "notify-days-before-expiry=30"
Automatic reload in ASP.NET Core
By default, AddAzureKeyVault loads secrets only once at startup. If you rotate a secret in Key Vault, the application will continue to use the old value until restarted. You can configure periodic reload:
builder.Configuration.AddAzureKeyVault(
new Uri(kvUri),
new DefaultAzureCredential(),
new AzureKeyVaultConfigurationOptions
{
// Reload secrets every 30 minutes
// Allows rotation without application restart
ReloadInterval = TimeSpan.FromMinutes(30)
});
With ReloadInterval set, the application automatically detects new versions. But it matters how you consume the configuration in services:
// IOptions -- value is fixed at startup, NOT updated
public StripeService(IOptions options) { ... }
// IOptionsSnapshot -- reloaded every request scope
// Compatible with ReloadInterval, but only in scoped/transient services
public StripeService(IOptionsSnapshot options) { ... }
// IOptionsMonitor -- value always current, safe even in singleton
public class StripeService
{
private readonly IOptionsMonitor _monitor;
public StripeService(IOptionsMonitor monitor)
{
_monitor = monitor;
_monitor.OnChange(opts =>
_logger.LogInformation("Stripe options updated"));
}
private string ApiKey => _monitor.CurrentValue.ApiKey;
}
Practical rule: use IOptionsMonitor<T> when the service is singleton (typical case for a reused HTTP client), IOptionsSnapshot<T> when the service is scoped per request.
Zero downtime rotation strategy
Zero downtime rotation requires a transition period during which both versions of the secret are valid. The recommended pattern:
- Generate the new API key at the provider (Stripe, SendGrid, etc.)
- Add the new version as a new secret in Key Vault (Key Vault keeps all versions)
- Wait for
ReloadIntervalto distribute the new value to all instances - Disable the old version in Key Vault
- Revoke the old key at the provider
# Add new version (old version remains accessible)
az keyvault secret set \
--vault-name my-app-keyvault \
--name "Stripe--ApiKey" \
--value "sk_live_new_value..."
# List versions of a secret
az keyvault secret list-versions \
--vault-name my-app-keyvault \
--name "Stripe--ApiKey"
# Disable an old version after transition
az keyvault secret set-attributes \
--vault-name my-app-keyvault \
--name "Stripe--ApiKey" \
--version "old-version-id" \
--enabled false
Certificates in Key Vault
Key Vault can also manage X.509 certificates — import, generation, automatic renewal, and distribution. Certificates stored in Key Vault are accessible to applications without copying them to disk.
Import existing certificate
az keyvault certificate import \
--vault-name my-app-keyvault \
--name "my-tls-cert" \
--file "./certificate.pfx" \
--password "pfx-password"
Generate certificate with automatic renewal
# Certificate policy with AutoRenew
cat > cert-policy.json << EOF
{
"issuerParameters": { "name": "Self" },
"keyProperties": { "keyType": "RSA", "keySize": 2048, "reuseKey": false },
"secretProperties": { "contentType": "application/x-pkcs12" },
"x509CertificateProperties": {
"subject": "CN=my-app.azurewebsites.net",
"validityInMonths": 12
},
"lifetimeActions": [{
"action": { "actionType": "AutoRenew" },
"trigger": { "daysBeforeExpiry": 30 }
}]
}
EOF
az keyvault certificate create \
--vault-name my-app-keyvault \
--name "my-tls-cert" \
--policy @cert-policy.json
The AutoRenew parameter in lifetimeActions causes Key Vault to automatically renew the certificate 30 days before expiration. No more surprise expired certificates.
Access certificates from .NET code
dotnet add package Azure.Security.KeyVault.Certificates
public class CertificateService
{
private readonly Uri _kvUri;
public CertificateService(IConfiguration config)
=> _kvUri = new Uri(config["KeyVaultUri"]!);
public async Task GetCertificateAsync(string certName)
{
// Certificates are also stored as secrets in Key Vault
// (PFX format is accessible via SecretClient)
var secretClient = new SecretClient(_kvUri, new DefaultAzureCredential());
var secret = await secretClient.GetSecretAsync(certName);
var certBytes = Convert.FromBase64String(secret.Value.Value);
return new X509Certificate2(certBytes, (string?)null,
X509KeyStorageFlags.MachineKeySet |
X509KeyStorageFlags.PersistKeySet |
X509KeyStorageFlags.Exportable);
}
}
Certificate for mTLS or client credentials
// HttpClient with client certificate from Key Vault
builder.Services.AddHttpClient("secure-api")
.ConfigurePrimaryHttpMessageHandler(sp =>
{
var certService = sp.GetRequiredService
What’s next
In the third part we cover granular access via RBAC at the individual secret level (the least privilege principle), full audit trail in Log Analytics, and Key Vault References in App Service — plus the complete pattern for configuration without any hardcoded secrets.
Questions? Write to me at contact@ludoprogramming.com.