--- status: Accepted date: 2026-09-09 applyTo: - "OCORE/OCORE/Secrets/**" - "OCORE_web/OCORE_web/Secrets/**" - "Fuchs/Program.cs" - "Fuchs/appsettings*.json" supersededBy: "" --- # 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.