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

77 lines
4.0 KiB
Markdown

---
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.