4.0 KiB
4.0 KiB
status, date, applyTo, supersededBy
| status | date | applyTo | supersededBy | ||||
|---|---|---|---|---|---|---|---|
| Accepted | 2026-09-09 |
|
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.mdunder "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_webadds a single thin file (Secrets/SecretManagementWebExtensions.cs) exposingWebApplicationBuilder.AddSecretManagement()for ASP.NET Core hosts; it calls back intoOCORE's shared logic rather than duplicating it. - Local DPAPI cache as offline fallback: secrets synced from Key Vault
are also encrypted to
secrets.cacheusing Windows DPAPI withDataProtectionScope.LocalMachineplus app-specific entropy derived fromAppName, 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) beforeFuchsOcmsIntranet.Initialize(configuration)— secrets must be resolved before any singleton readsIConfiguration. - New secrets must be added to Key Vault under the
fuchs--prefix and registered inManagedSecretKeysinFuchs/appsettings.json(see.github/copilot-instructions.md), orKeyVaultSyncServicewill never pick them up. - A secret whose name is only known at runtime (not a fixed config key)
bypasses
ManagedSecretKeys/KeyVaultSyncServiceentirely and is fetched directly viaOCORE.Secrets.Extensions.SecretsOnDemandLoaderinstead. - Any other application added to the shared server follows the same
pattern: reference
OCORE_web(web) orOCORE(non-web), set a uniqueAppName, and grant its prefixGet/Listaccess 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.