Secure Secret Management with Azure Key Vault in .NET
A confession to start: I’ve shipped connection strings in appsettings.json more times than I’m proud of. The reasons are always the same — “it’s only dev”, “we’ll fix it before prod”, “the repo is private.” None of those reasons survive the first time a secret leaks. Key Vault is the cure, and once it’s wired into your app correctly with managed identity, you stop touching secrets in code entirely. Here’s the integration that I now reach for as the default.
Creating a Key Vault
# Create a Key Vault
az keyvault create \
--name kv-myapp-2020 \
--resource-group rg-security \
--location australiaeast \
--enable-soft-delete true \
--enable-purge-protection true
# Add a secret
az keyvault secret set \
--vault-name kv-myapp-2020 \
--name "DatabaseConnectionString" \
--value "Server=tcp:myserver.database.windows.net..."
# Add another secret
az keyvault secret set \
--vault-name kv-myapp-2020 \
--name "ApiKey" \
--value "your-api-key-here"
Managed Identity Setup
Use managed identity for secure, credential-free access:
# Enable managed identity on App Service
az webapp identity assign \
--name mywebapp \
--resource-group rg-app
# Get the principal ID
principalId=$(az webapp identity show \
--name mywebapp \
--resource-group rg-app \
--query principalId -o tsv)
# Grant access to Key Vault
az keyvault set-policy \
--name kv-myapp-2020 \
--object-id $principalId \
--secret-permissions get list
.NET Core Configuration Integration
Add the NuGet packages:
dotnet add package Azure.Extensions.AspNetCore.Configuration.Secrets
dotnet add package Azure.Identity
Configure in Program.cs:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
var builtConfig = config.Build();
var keyVaultEndpoint = new Uri($"https://{builtConfig["KeyVaultName"]}.vault.azure.net/");
config.AddAzureKeyVault(keyVaultEndpoint, new DefaultAzureCredential());
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
In appsettings.json:
{
"KeyVaultName": "kv-myapp-2020"
}
Accessing Secrets
Secrets are available through standard configuration:
public class MyService
{
private readonly string _connectionString;
private readonly string _apiKey;
public MyService(IConfiguration configuration)
{
// Key Vault secret names use -- instead of : for hierarchy
_connectionString = configuration["DatabaseConnectionString"];
_apiKey = configuration["ApiKey"];
}
}
Direct Key Vault Client Usage
For more control, use the SDK directly:
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
public class KeyVaultService
{
private readonly SecretClient _client;
public KeyVaultService(string keyVaultName)
{
var keyVaultUri = new Uri($"https://{keyVaultName}.vault.azure.net/");
_client = new SecretClient(keyVaultUri, new DefaultAzureCredential());
}
public async Task<string> GetSecretAsync(string secretName)
{
var secret = await _client.GetSecretAsync(secretName);
return secret.Value.Value;
}
public async Task SetSecretAsync(string secretName, string value)
{
await _client.SetSecretAsync(secretName, value);
}
public async Task<List<string>> ListSecretsAsync()
{
var secrets = new List<string>();
await foreach (var secret in _client.GetPropertiesOfSecretsAsync())
{
secrets.Add(secret.Name);
}
return secrets;
}
}
Working with Certificates
using Azure.Security.KeyVault.Certificates;
public class CertificateService
{
private readonly CertificateClient _client;
public CertificateService(string keyVaultName)
{
var keyVaultUri = new Uri($"https://{keyVaultName}.vault.azure.net/");
_client = new CertificateClient(keyVaultUri, new DefaultAzureCredential());
}
public async Task<X509Certificate2> GetCertificateAsync(string certificateName)
{
var certificate = await _client.GetCertificateAsync(certificateName);
return new X509Certificate2(certificate.Value.Cer);
}
}
Caching Secrets
For performance, implement caching:
public class CachedKeyVaultService
{
private readonly SecretClient _client;
private readonly IMemoryCache _cache;
private readonly TimeSpan _cacheExpiration = TimeSpan.FromMinutes(5);
public CachedKeyVaultService(SecretClient client, IMemoryCache cache)
{
_client = client;
_cache = cache;
}
public async Task<string> GetSecretAsync(string secretName)
{
return await _cache.GetOrCreateAsync($"kv-{secretName}", async entry =>
{
entry.AbsoluteExpirationRelativeToNow = _cacheExpiration;
var secret = await _client.GetSecretAsync(secretName);
return secret.Value.Value;
});
}
}
Local Development
For local development, use Azure CLI authentication:
# Login to Azure
az login
# Set your subscription
az account set --subscription "your-subscription"
The DefaultAzureCredential will automatically use your Azure CLI credentials locally.
Secret Rotation
Implement secret rotation handling:
public class RotatingSecretService
{
private readonly SecretClient _client;
private string _currentSecret;
private DateTimeOffset _lastRefresh;
private readonly TimeSpan _refreshInterval = TimeSpan.FromHours(1);
public async Task<string> GetSecretAsync(string secretName)
{
if (_currentSecret == null ||
DateTimeOffset.UtcNow - _lastRefresh > _refreshInterval)
{
var secret = await _client.GetSecretAsync(secretName);
_currentSecret = secret.Value.Value;
_lastRefresh = DateTimeOffset.UtcNow;
}
return _currentSecret;
}
}
Lessons learned the painful way
- Soft-delete and purge protection should be on for any vault you care about. Without them, a misconfigured Terraform run can wipe a vault, and “restore from backup” is not a thing for Key Vault data.
- Use Managed Identity wherever possible. The whole point of Key Vault is to not have credentials in your app — and then plenty of tutorials hand you a client secret to authenticate to Key Vault, which puts you back where you started.
- RBAC roles are now preferred over access policies. New vaults default to RBAC; existing vaults can be migrated. RBAC integrates with Privileged Identity Management, which you’ll want once auditors get involved.
- Don’t read every secret on every request. Cache (and refresh on a schedule), or use the Configuration Builder so secrets are loaded at startup.
Key Vault is not interesting technology. It’s invisible-when-it-works infrastructure, like good plumbing. Set it up once, with managed identity, RBAC, soft-delete, and purge protection, and never think about it again.\n\n## Takeaways\n\nAdd a concise, personal takeaway and recommended next steps here.\n