Q23 — Secrets Management — User Secrets, Azure Key Vault, Managed Identity
Level: Mid-Level | Topic: Security
The Problem
Secrets committed to source control are a leading cause of breaches. The correct approach: use different secret stores per environment, with zero credentials needed in production code via Managed Identity.
Environment Strategy
- Development:
dotnet user-secrets— stored outside the project directory in~/.microsoft/usersecrets/, never committed to git. - CI/CD: Environment variables or GitHub Actions Secrets / Azure DevOps Variable Groups — injected at build time, not stored in code.
- Staging/Production: Azure Key Vault (or AWS Secrets Manager / HashiCorp Vault) accessed via Managed Identity — no client secrets needed at all.
Code Example
# Development — dotnet user-secrets
dotnet user-secrets init --project src/Api
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Server=...;..."
dotnet user-secrets set "Jwt:Key" "super-secret-dev-key-32chars!!"
dotnet user-secrets list
// Production — Azure Key Vault via Managed Identity (zero credentials in code)
// Packages: Azure.Extensions.AspNetCore.Configuration.Secrets, Azure.Identity
public static IHostBuilder AddSecretsManagement(this IHostBuilder host)
{
host.ConfigureAppConfiguration((ctx, config) =>
{
if (ctx.HostingEnvironment.IsDevelopment())
{
config.AddUserSecrets<Program>(); // dev only
return;
}
// Production: Key Vault via Managed Identity — NO credentials in code!
var keyVaultUri = new Uri(ctx.Configuration["KeyVault:Uri"]
?? throw new InvalidOperationException("KeyVault:Uri not configured"));
config.AddAzureKeyVault(
keyVaultUri,
new DefaultAzureCredential(), // uses Managed Identity in Azure
new AzureKeyVaultConfigurationOptions
{
ReloadInterval = TimeSpan.FromMinutes(5) // auto-reload rotated secrets
});
});
return host;
}
// Strongly-typed access with startup validation
builder.Services.AddOptions<DatabaseOptions>()
.BindConfiguration(DatabaseOptions.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart(); // fail fast on missing secrets at startup
// Secret rotation — IOptionsMonitor picks up changes without restart
public class RotatingSecretService
{
private readonly IOptionsMonitor<DatabaseOptions> _optionsMonitor;
public RotatingSecretService(IOptionsMonitor<DatabaseOptions> monitor)
{
_optionsMonitor = monitor;
_optionsMonitor.OnChange(opts =>
Console.WriteLine("Connection string rotated — reconnecting pool"));
}
public string CurrentConnectionString
=> _optionsMonitor.CurrentValue.DefaultConnection;
}
Senior Insight
Managed Identity is the gold standard for production — it eliminates entire categories of secrets (no client IDs, no client secrets, no rotation concerns for the identity itself). Use ValidateOnStart() with ValidateDataAnnotations() so misconfigured secrets cause a startup failure with a clear error rather than a runtime exception in production. Set ReloadInterval on Key Vault configuration so rotated secrets are picked up without a restart.