77 lines
4.0 KiB
Markdown
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.
|