Files
Fuchs_Intranet/Fuchs/Docs/Decisions/0031-shared-keyvault-dpapi-secret-management.md
2026-09-18 14:14:56 +02:00

4.0 KiB

status, date, applyTo, supersededBy
status date applyTo supersededBy
Accepted 2026-09-09
OCORE/OCORE/Secrets/**
OCORE_web/OCORE_web/Secrets/**
Fuchs/Program.cs
Fuchs/appsettings*.json

0031 — Shared Key Vault + DPAPI secret management via OCORE/OCORE_web

Context

Fuchs is one of several applications (web apps, console apps, Windows services, worker services) running on the same Windows server. Secrets (connection strings, API keys, mailer tokens, etc.) previously lived in plain text in appsettings.json/web.config/environment variables. A single design needed to work identically across all host types — not just ASP.NET Core — while keeping secrets out of source control and available even when the Key Vault is briefly unreachable.

Decision

  • One shared Azure Key Vault (pcwkeys) for all applications on the server, rather than a per-app vault. Access control happens at the secret level (RBAC), not at the vault level.
  • {appname}-- prefix namespacing on every secret name (e.g. fuchs--ConnectionStrings--ocms-ConnectionString) prevents collisions between apps sharing the vault. Naming/encoding rules are recorded in .github/copilot-instructions.md under "Azure Key Vault — Secret Naming".
  • Core logic lives in OCORE (OCORE/Secrets/Models/AppSecretsOptions.cs, Cache/DpapiSecretsCache.cs, Cache/DpapiCacheConfigurationSource.cs, Sync/KeyVaultSyncService.cs, Extensions/SecretManagementExtensions.cs, Extensions/PrefixKeyVaultSecretManager.cs, Extensions/SecretsOnDemandLoader.cs) because it has no web dependency and must also work for console apps, Windows services, worker services, and WinForms/WPF. OCORE_web adds a single thin file (Secrets/SecretManagementWebExtensions.cs) exposing WebApplicationBuilder.AddSecretManagement() for ASP.NET Core hosts; it calls back into OCORE's shared logic rather than duplicating it.
  • Local DPAPI cache as offline fallback: secrets synced from Key Vault are also encrypted to secrets.cache using Windows DPAPI with DataProtectionScope.LocalMachine plus app-specific entropy derived from AppName, so one app on the server cannot decrypt another app's cache file, and the app can still start if Key Vault is briefly unreachable.
  • One shared server-level Managed Identity (via DefaultAzureCredential) for all apps, rather than 15 separate identities — access is scoped by the {appname}-- secret prefix via Key Vault RBAC, not by identity.

Consequences

  • Fuchs calls builder.AddSecretManagement() (Fuchs/Program.cs) before FuchsOcmsIntranet.Initialize(configuration) — secrets must be resolved before any singleton reads IConfiguration.
  • New secrets must be added to Key Vault under the fuchs-- prefix and registered in ManagedSecretKeys in Fuchs/appsettings.json (see .github/copilot-instructions.md), or KeyVaultSyncService will never pick them up.
  • A secret whose name is only known at runtime (not a fixed config key) bypasses ManagedSecretKeys/KeyVaultSyncService entirely and is fetched directly via OCORE.Secrets.Extensions.SecretsOnDemandLoader instead.
  • Any other application added to the shared server follows the same pattern: reference OCORE_web (web) or OCORE (non-web), set a unique AppName, and grant its prefix Get/List access in the shared vault.
  • Detailed wiring/encoding reference (kept current independently of this ADR) lives in OCORE_web/.github_instructions/secretsmanagement.instructions.md.

Alternatives considered

  • Per-app Key Vaults — rejected: 15x the management overhead for no isolation benefit over prefix namespacing + secret-level RBAC.
  • Per-app Managed Identities — rejected: requires provisioning and maintaining 15 separate identities; a shared identity with per-secret RBAC achieves the same isolation.
  • Environment variables only, no local cache — rejected: no offline/startup resilience if Key Vault is briefly unreachable.