Managed Secrets and Identity

Load secrets from Key Vault or Secrets Manager as configuration, prove who you are with a role, and rotate without a redeploy.

Syntaxbuilder.Configuration.AddAzureKeyVault(new Uri(vaultUri), new DefaultAzureCredential());

A secret is a value that grants access: a database password, an API key, a signing key. Secrets do not belong in appsettings.json, because that file is in git and baked into the image. A managed secret store holds them instead. AWS calls its store Secrets Manager; Azure calls its store Key Vault.

A secret store is a configuration provider

You do not have to change how your code reads settings. A configuration provider is one source that IConfiguration reads from, and the store becomes one more source layered on top of the files. config["ConnectionStrings:Default"] keeps working whether the value came from a file, an environment variable or the vault.

Identity instead of another secret

Reading the store needs permission, and a password for the password store would solve nothing. So the platform gives the compute itself an identity: an IAM role attached to the pod's service account on EKS, a managed identity on Azure. The SDK trades that identity for a short-lived token. There is no credential for you to store, ship or rotate.

Rotation without a redeploy

Set a reload interval and the provider re-reads the store on a timer, so a rotated value reaches a running process. Code that read the value once at startup still holds the old one. Read through IConfiguration or IOptionsMonitor<T>, never a value copied into a field at construction.

What trips people up

Reloading is neither instant nor free; each interval is one call per provider, per process.

Example

Example · csharp
// dotnet add package Azure.Extensions.AspNetCore.Configuration.Secrets
// dotnet add package Azure.Identity
using Azure.Extensions.AspNetCore.Configuration.Secrets;
using Azure.Identity;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

// The vault address is not a secret, so it lives in ordinary configuration.
string vault = builder.Configuration["KeyVault:Uri"]
    ?? throw new InvalidOperationException("KeyVault:Uri is not set.");

// DefaultAzureCredential uses the app's managed identity when deployed and
// your Azure CLI sign-in locally. ReloadInterval re-reads the vault on a
// timer, so a rotated secret reaches this process without a redeploy.
builder.Configuration.AddAzureKeyVault(
    new Uri(vault),
    new DefaultAzureCredential(),
    new AzureKeyVaultConfigurationOptions
    {
        ReloadInterval = TimeSpan.FromMinutes(5)
    });

WebApplication app = builder.Build();

// Read through IConfiguration, so this sees the most recent reload.
// The vault secret behind this key is named ConnectionStrings--Default.
app.MapGet("/health/secrets", (IConfiguration config) =>
{
    bool present = !string.IsNullOrEmpty(config["ConnectionStrings:Default"]);
    return Results.Ok(new { connectionStringPresent = present });
});

app.Run();

When to use it

  • Keep the database password out of the container image and out of git.
  • Let an EKS pod read an S3 bucket with no access key anywhere in the deployment.
  • Rotate a payment provider's API key overnight without restarting every pod.

More examples

Reading one secret from AWS Secrets Manager

Use the direct read when you need one secret at a time, or when the value is not a configuration setting at all. Call it once at startup and cache the result; Secrets Manager charges per call and will throttle a process that asks on every request.

Example · csharp
// dotnet add package AWSSDK.SecretsManager
using System.Text.Json;
using Amazon.SecretsManager;
using Amazon.SecretsManager.Model;

// No key here either. On EKS the pod's service account is annotated with an
// IAM role, and the SDK picks up the temporary credentials that gives it.
IAmazonSecretsManager secrets = new AmazonSecretsManagerClient();

GetSecretValueResponse response = await secrets.GetSecretValueAsync(
    new GetSecretValueRequest { SecretId = "prod/orders/db" });

// Secrets Manager stores database secrets as a JSON document by convention.
DbSecret secret = JsonSerializer.Deserialize<DbSecret>(response.SecretString)
    ?? throw new InvalidOperationException("prod/orders/db was empty");

public sealed record DbSecret(string Username, string Password, string Host);

Seeing a rotated value in a long-lived object

This is the half of rotation that is in your code rather than the platform's. A primary constructor and an explicit constructor compile to the same thing, so pick whichever reads better in the file. What matters is that the value is read per use, not captured once.

Example · csharp
using Microsoft.Extensions.Options;

public sealed class SmtpOptions
{
    public string Host { get; set; } = "";
    public string Password { get; set; } = "";
}

// IOptions<T> is a snapshot taken once. A singleton holding one never sees
// a rotation. IOptionsMonitor<T> re-reads configuration, so CurrentValue
// reflects the last reload.
public sealed class MailSender(IOptionsMonitor<SmtpOptions> options)
{
    public string CurrentHost => options.CurrentValue.Host;
}

// The long form of the same class, for comparison:
// public sealed class MailSender
// {
//     private readonly IOptionsMonitor<SmtpOptions> _options;
//     public MailSender(IOptionsMonitor<SmtpOptions> options) => _options = options;
//     public string CurrentHost => _options.CurrentValue.Host;
// }

The same keys locally, with no vault

Developers need working values without access to the production vault. Because both sources feed the same configuration keys, the code that reads them is identical in both places, and nothing secret is ever written into the repository.

Example · csharp
// In the project folder, once:
//   dotnet user-secrets init
//   dotnet user-secrets set "ConnectionStrings:Default" "Host=localhost;Database=orders"
//
// The user-secrets store is a JSON file in your profile, outside the repo.
// CreateBuilder adds it automatically in the Development environment.

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

if (!builder.Environment.IsDevelopment())
{
    // Only deployed environments go to the vault.
    builder.Configuration.AddAzureKeyVault(
        new Uri(builder.Configuration["KeyVault:Uri"]!),
        new DefaultAzureCredential());
}

Discussion

  • Be the first to comment on this lesson.