Back to all posts

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 config output.

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 Administrator to 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.