Learnixo
Back to blog
AI Systemsintermediate

Azure Key Vault — Secrets, Keys, and Certificates in .NET

Use Azure Key Vault in .NET applications: storing secrets, injecting Key Vault into IConfiguration, Managed Identity authentication, key rotation, and certificate management for clinical APIs.

Asma Hafeez KhanMay 16, 20264 min read
AzureKey VaultSecurity.NETSecrets Management
Share:𝕏

Why Key Vault

Without Key Vault:
  appsettings.Production.json:
    "ExternalApi": { "ApiKey": "sk-live-abc123..." }
  
  Problems:
  → Secret is in source control — every developer who cloned the repo has it
  → Rotating the secret requires a code deployment
  → No audit trail of who read the secret or when
  → Secret lives in plain text on the server's filesystem

With Key Vault:
  appsettings.json:
    "ExternalApi": { "ApiKey": "@Microsoft.KeyVault(SecretUri=https://clinical-kv.vault.azure.net/secrets/ExternalApiKey)" }
    or, via AddAzureKeyVault: key vault secrets appear directly in IConfiguration

  Benefits:
  → Secret never stored in source control
  → Rotation: update in Key Vault, application picks it up without redeployment
  → Full audit log: who read which secret and when
  → RBAC: developer Bob can write secrets, production App Service can only read them

Integrating Key Vault with IConfiguration

C#
// Program.cs — add Key Vault as a configuration source
// Secrets become available via IConfiguration like any other setting

var keyVaultUri = new Uri(builder.Configuration["AzureKeyVault:Uri"]!);

builder.Configuration.AddAzureKeyVault(
    keyVaultUri,
    new DefaultAzureCredential());

// DefaultAzureCredential:
// → In production (App Service): uses Managed Identity automatically
// → In local development: uses Azure CLI credentials (az login)
// → In CI/CD: uses environment variables (AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID)

// Now read secrets the same way as any configuration value:
var apiKey = builder.Configuration["ExternalApi--ApiKey"];
// Key Vault secret name: "ExternalApi--ApiKey" (double-dash = colon in IConfiguration)
// This maps to: IConfiguration["ExternalApi:ApiKey"]

Secret Naming Convention

Key Vault secret names must match .NET IConfiguration key paths.
IConfiguration uses ":" as the separator: "ConnectionStrings:Clinical"
Key Vault does not allow ":" in secret names — use "--" (double dash) instead.

IConfiguration key               → Key Vault secret name
ConnectionStrings:Clinical       → ConnectionStrings--Clinical
ExternalApi:ApiKey               → ExternalApi--ApiKey
Jwt:SigningKey                   → Jwt--SigningKey
Fhir:BaseUrl                     → Fhir--BaseUrl
Smtp:Password                    → Smtp--Password

When AddAzureKeyVault is registered, it automatically maps "--" back to ":"
so builder.Configuration["ConnectionStrings:Clinical"] works as expected.

Setting Secrets with Azure CLI

Bash
# Create or update a secret
az keyvault secret set \
  --vault-name clinical-kv \
  --name "ConnectionStrings--Clinical" \
  --value "Server=clinical-sql.database.windows.net;Initial Catalog=ClinicalDb;Authentication=Active Directory Managed Identity"

# Read a secret (for verification)
az keyvault secret show \
  --vault-name clinical-kv \
  --name "ConnectionStrings--Clinical" \
  --query "value" -o tsv

# List all secrets (shows names only, not values)
az keyvault secret list --vault-name clinical-kv --query "[].name" -o table

# Grant App Service read access to Key Vault
az keyvault set-policy \
  --name clinical-kv \
  --object-id $(az webapp identity show --name clinical-platform --resource-group clinical-rg --query principalId -o tsv) \
  --secret-permissions get list

Accessing Secrets Programmatically

C#
// Sometimes you need to access a secret directly (e.g., for rotation checks)
// rather than via IConfiguration injection

public sealed class ExternalApiClient
{
    private readonly SecretClient _secretClient;
    private readonly HttpClient   _httpClient;

    public ExternalApiClient(SecretClient secretClient, HttpClient httpClient)
    {
        _secretClient = secretClient;
        _httpClient   = httpClient;
    }

    public async Task<string> GetMedicationDataAsync(string medicationCode, CancellationToken ct)
    {
        // Fetch the API key fresh each time (supports key rotation without restart)
        var secret = await _secretClient.GetSecretAsync("ExternalApi--ApiKey", null, ct);
        _httpClient.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", secret.Value.Value);

        var response = await _httpClient.GetAsync($"/medications/{medicationCode}", ct);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync(ct);
    }
}

// DI registration:
builder.Services.AddSingleton(_ =>
    new SecretClient(
        new Uri("https://clinical-kv.vault.azure.net/"),
        new DefaultAzureCredential()));

Certificates in Key Vault

C#
// Store TLS certificates in Key Vault — automatic renewal with App Service

// App Service → TLS/SSL settings → Import certificate from Key Vault
// The certificate is rotated in Key Vault and App Service picks it up automatically

// For custom certificate loading (e.g., for mTLS to downstream services):
public static X509Certificate2 LoadCertificate(CertificateClient certClient, string name)
{
    var certWithPolicy = certClient.GetCertificate(name);
    var secretClient   = new SecretClient(
        new Uri("https://clinical-kv.vault.azure.net/"), new DefaultAzureCredential());

    // Certificate is stored as a secret (PFX format) in Key Vault
    var secret = secretClient.GetSecret(certWithPolicy.Value.SecretId.AbsolutePath.TrimStart('/'));
    var pfxBytes = Convert.FromBase64String(secret.Value.Value);
    return new X509Certificate2(pfxBytes);
}

Production issue I've seen: A team stored 14 secrets across 3 environments in appsettings.{Environment}.json files. These were committed to the repository with access controls, but a junior developer accidentally included them in a PR that was temporarily opened as a public repository for a portfolio demo. All 14 secrets were exposed within 3 minutes (automated scanners). Rotation required contacting 5 external vendors (FHIR API, SMS gateway, payment processor, email service, analytics) during an out-of-hours incident call. The rotation took 11 hours. Migrating all secrets to Key Vault before go-live would have meant the repository contained no secrets to expose.


Key Takeaway

Never store secrets in source control or configuration files. Add Azure Key Vault as an IConfiguration source via AddAzureKeyVault — secrets become available transparently via IConfiguration with no code changes. Use Managed Identity (DefaultAzureCredential) — no client secrets needed to access Key Vault. Use the -- double-dash naming convention to map Key Vault secret names to IConfiguration paths. Grant the App Service Managed Identity only get and list permissions on secrets — not set or delete.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.