Skip to main content

Secrets provider switch (SecretsProvider)

Shipped 2026-08-26 (extended 2026-08-27), replacing the old implicit "blank KeyVault:Uri means no-op" signal with an explicit config discriminator — same convention as Caching:ListCache/Auth:RefreshTokenStore/ Sso:PendingLoginStore.

The switch

SecretConfigurationExtensions.AddEosSecrets(this IConfigurationBuilder, IConfiguration) (Infrastructure/Configuration/) is the one seam every composition root calls — unchanged in shape (builder.Configuration.AddEosSecrets(builder.Configuration), first statement after WebApplication.CreateBuilder(args), before AddApplication()/AddInfrastructure()). It reads a top-level SecretsProvider key:

  • Unset / blank / "env" (case-insensitive) — a documented no-op. .NET's default provider chain already includes AddEnvironmentVariables(), so nothing new happens here.
  • "keyvault" — the existing AddAzureKeyVault + ManagedIdentityCredential (SystemAssigned) mechanism, now gated behind the explicit key instead of being inferred from KeyVault:Uri alone.
  • "vault" — HashiCorp Vault via VaultSharp (shipped 2026-08-27, the deliberate second phase this switch was built to accept). AppRole auth only (Vault:RoleId/Vault:SecretId) — no static-token auth path exists or is planned.
  • Anything else — fails fast, listing the valid values.

:::tip ManagedIdentityCredential(SystemAssigned), never DefaultAzureCredential DefaultAzureCredential probes multiple credential sources in order before it succeeds, adding real cold-start latency even when it eventually reaches managed identity. ManagedIdentityCredential(SystemAssigned) goes straight to the IMDS endpoint, skipping that probe chain. Follow this same preference for any future Azure credential acquisition in this codebase. :::

Fail-fast, not silent

SecretsProvider=keyvault with a blank KeyVault:Uri throws an InvalidOperationException naming the missing key before app.Run() — the same "worse to start wrong than not start" posture EosDatabaseMigrator already uses. An unrecognized SecretsProvider value throws too, listing valid values.

HashiCorp Vault (SecretsProvider=vault)

For Model C clients with enterprise IT already running Vault. Requires Vault:RoleId/Vault:SecretId (AppRole), Vault:Address, and Vault:SecretPath (the KV v2 secret path); Vault:MountPoint defaults to "secret".

:::caution Single blocking read at startup, not lazy Unlike AddAzureKeyVault's lazy provider, this is a single blocking KV v2 read at startup (VaultClient.V1.Secrets.KeyValue.V2.ReadSecretAsync(...) .GetAwaiter().GetResult()) — VaultSharp is async-only and IConfigurationBuilder.Build() is synchronous. Vault secret data keys must already be full IConfiguration keys (e.g. a secret literally named "ConnectionStrings:EosDb") — there's no --/__-style escaping to apply, since Vault keys can contain : directly. A read failure (bad address, invalid AppRole credentials, missing path) is caught and wrapped in a clear InvalidOperationException naming the provider/path. :::

Not verified against a real Vault server — no HashiCorp Vault instance exists in this environment or for any real Model C client yet.

Backward compatible, no consumer changes

Every current caller of IConfiguration (the Jwt:SigningKey read, DependencyInjection.cs's GetConnectionString("EosDb"), the OpenTelemetry Azure Monitor exporter's connection-string read) stays completely unaware of which provider supplied a value — only the three Program.cs call sites changed, from passing a single KeyVault:Uri string to passing the whole IConfiguration. Every existing appsettings.json in this repo has no SecretsProvider key today, so every deployment that relied on a blank/ non-blank KeyVault:Uri no-op keeps working unmodified.

Reference files

SecretConfigurationExtensions.cs, docs-internal/keyvault-provisioning-runbook.md, SecretConfigurationExtensionsTests.cs, Api/Program.cs/Workers/Program.cs/ Api.External/Program.cs (the AddEosSecrets call site).