Secrets usage by Keyvault
No secrets in source code
Azure Cloud Infrastructure: Stop Storing Secrets in Config Files
The Problem
A very common (and dangerous) pattern in .NET apps deployed to Azure:
// appsettings.json — DO NOT DO THIS
{
"ConnectionStrings": {
"SqlDb": "Server=tcp:myserver.database.windows.net;Database=mydb;User Id=admin;Password=P@ssw0rd123!;"
},
"StripeApiKey": "sk_live_51H..."
}
This causes real problems:
- Secrets get committed to source control (even briefly, they're in git history forever).
- No rotation story — changing a password means redeploying every app that uses it.
- Anyone with read access to the App Service configuration (or the repo) sees production credentials.
- Environment variables aren't much better — they show up in process dumps, CI logs, and
az webapp configoutput.
The Solution: Managed Identity + Azure Key Vault
Instead of the app knowing a secret, give the app an identity that Azure AD trusts, and let it ask Key Vault for secrets at runtime. No credentials are ever stored anywhere in your code or config.
sequenceDiagram
participant App as App Service / Function
participant MI as Managed Identity
participant AAD as Azure AD
participant KV as Azure Key Vault
App->>MI: Request access token (no secrets needed)
MI->>AAD: Authenticate using platform-managed identity
AAD-->>MI: Access token (scoped to Key Vault)
MI-->>App: Token
App->>KV: GetSecret("SqlConnectionString") + token
KV->>KV: Check RBAC role assignment
KV-->>App: Secret value (in memory only)
Step 1 — Enable Managed Identity (Bicep)
resource appService 'Microsoft.Web/sites@2023-12-01' = {
name: 'my-app'
location: resourceGroup().location
identity: {
type: 'SystemAssigned'
}
properties: {
// ... site config
}
}
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
name: 'my-keyvault'
}
// Grant the App Service's identity permission to read secrets — RBAC, not access policies
resource kvRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(keyVault.id, appService.id, 'KeyVaultSecretsUser')
scope: keyVault
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6') // Key Vault Secrets User
principalId: appService.identity.principalId
principalType: 'ServicePrincipal'
}
}
Step 2 — Load secrets straight into IConfiguration (zero code changes elsewhere)
using Azure.Identity;
var builder = WebApplication.CreateBuilder(args);
var keyVaultUri = new Uri($"https://{builder.Configuration["KeyVaultName"]}.vault.azure.net/");
// DefaultAzureCredential automatically uses the Managed Identity in Azure,
// and falls back to Azure CLI / VS credentials when running locally.
builder.Configuration.AddAzureKeyVault(keyVaultUri, new DefaultAzureCredential());
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration["SqlConnectionString"]));
var app = builder.Build();
app.Run();
Every secret in the vault (e.g. SqlConnectionString, StripeApiKey) now appears in IConfiguration exactly like an appsettings value — nothing downstream needs to know it came from Key Vault.
Step 3 — Explicit retrieval with caching (for secrets fetched on demand)
Key Vault has request throttling limits, so cache secrets rather than calling it on every request:
public class SecretService
{
private readonly SecretClient _client;
private readonly IMemoryCache _cache;
public SecretService(IConfiguration config, IMemoryCache cache)
{
_client = new SecretClient(
new Uri(config["KeyVault:Uri"]!),
new DefaultAzureCredential());
_cache = cache;
}
public async Task<string> GetSecretAsync(string name)
{
return await _cache.GetOrCreateAsync(name, async entry =>
{
entry.SlidingExpiration = TimeSpan.FromMinutes(30);
KeyVaultSecret secret = await _client.GetSecretAsync(name);
return secret.Value;
}) ?? throw new InvalidOperationException($"Secret '{name}' not found.");
}
}
Why This Matters
| Aspect | Config/Env Var Secrets | Managed Identity + Key Vault |
|---|---|---|
| Credential storage | In repo/App Service config | Never stored — obtained at runtime |
| Rotation | Manual redeploy of every consumer | Update once in Key Vault, apps pick it up |
| Auditability | None | Full audit log of every secret access (who/when) |
| Leak blast radius | Whole secret exposed if config leaks | Attacker needs a valid Azure AD token scoped to the vault |
| Local dev | Secrets duplicated on every machine | DefaultAzureCredential uses your az login session |
Checklist
- Use RBAC role assignments (
Key Vault Secrets User), not legacy access policies. - Enable soft-delete and purge protection on the vault.
- Never grant broader roles like
Key Vault Administratorto an app identity — least privilege. - Prefer App Service Key Vault references (
@Microsoft.KeyVault(SecretUri=...)) for simple cases where you don't even want the app to call the SDK. - Cache secret values in memory; don't re-fetch on every request.