feat: Add documentation for secret management using Azure Key Vault and DPAPI
Playwright Tests / test (push) Canceled after 0s
Playwright Tests / test (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
---
|
||||
status: Active
|
||||
lastUpdated: 2026-09-18
|
||||
applyTo:
|
||||
- "Fuchs/Program.cs"
|
||||
- "Fuchs/appsettings*.json"
|
||||
- "OCORE/OCORE/Secrets/**"
|
||||
- "OCORE_web/OCORE_web/Secrets/**"
|
||||
relatedDecisions:
|
||||
- "0031-shared-keyvault-dpapi-secret-management.md"
|
||||
---
|
||||
|
||||
# Secret management (Key Vault + DPAPI)
|
||||
|
||||
## Summary
|
||||
Fuchs loads its secrets (connection strings, API keys, mailer tokens, ...)
|
||||
from the shared Azure Key Vault `pcwkeys` at startup, with a DPAPI-encrypted
|
||||
local cache as an offline fallback. The mechanism is shared, host-type-agnostic
|
||||
infrastructure owned by the `OCORE`/`OCORE_web` submodules ([0031](../Decisions/0031-shared-keyvault-dpapi-secret-management.md));
|
||||
this doc covers how Fuchs specifically wires into it and where to make
|
||||
changes. The full mechanism reference (naming/encoding rules, DI wiring,
|
||||
cache format) is maintained independently in
|
||||
`OCORE_web/.github_instructions/secretsmanagement.instructions.md`.
|
||||
|
||||
## How it works
|
||||
1. `Fuchs/Program.cs` calls `builder.AddSecretManagement()`
|
||||
(`OCORE_web.Secrets.SecretManagementWebExtensions`) as the first step in
|
||||
`ConfigureServices`, before `FuchsOcmsIntranet.Initialize(configuration)`
|
||||
or anything else reads `IConfiguration`.
|
||||
2. That call, backed by `OCORE.Secrets.Extensions.SecretManagementExtensions`,
|
||||
registers two configuration sources in order: the local DPAPI cache
|
||||
(`secrets.cache`, offline fallback) first, then the live Azure Key Vault
|
||||
(overrides the cache when reachable) using `PrefixKeyVaultSecretManager`
|
||||
to filter to secrets named `fuchs--...` and map them onto
|
||||
`IConfiguration` keys.
|
||||
3. `KeyVaultSyncService` (a registered `BackgroundService`) periodically
|
||||
re-pulls every key listed in `Fuchs:SecretManagement:ManagedSecretKeys`
|
||||
from Key Vault, re-writes the DPAPI cache, and calls
|
||||
`IConfigurationRoot.Reload()` so a running process picks up rotated
|
||||
secrets without a restart.
|
||||
4. A secret whose name is only known at runtime (not a fixed
|
||||
`IConfiguration` key) — e.g. resolved from a DB job record — is instead
|
||||
fetched directly via `OCORE.Secrets.Extensions.SecretsOnDemandLoader`,
|
||||
bypassing `ManagedSecretKeys` and the periodic sync entirely.
|
||||
5. In `Development`, `appsettings.Development.json` (git-ignored) can
|
||||
override any of these values locally, bypassing Key Vault for that key;
|
||||
`AssembleConnectionStrings` (`Fuchs/Program.cs`) additionally prefers
|
||||
`_Dev`-suffixed credential keys so a reachable Key Vault can never hand a
|
||||
dev machine production DB credentials.
|
||||
|
||||
## Key files
|
||||
- `Fuchs/Program.cs` — `builder.AddSecretManagement()` call site and
|
||||
`AssembleConnectionStrings`.
|
||||
- `Fuchs/appsettings.json` — `SecretManagement` block (`VaultUri`,
|
||||
`AppName: "fuchs"`, `CacheFilePath`, `SyncIntervalHours`,
|
||||
`ManagedSecretKeys`) plus `"MANAGED_BY_KEYVAULT"` placeholders for every
|
||||
managed key.
|
||||
- `OCORE/Secrets/Extensions/SecretManagementExtensions.cs` — shared
|
||||
Key Vault + DPAPI wiring and DI registration, used by every host type.
|
||||
- `OCORE/Secrets/Extensions/SecretsOnDemandLoader.cs` — direct, cached,
|
||||
runtime-name secret lookup outside the `ManagedSecretKeys` sync path.
|
||||
- `OCORE/Secrets/Sync/KeyVaultSyncService.cs` — periodic re-sync +
|
||||
`IConfigurationRoot.Reload()`.
|
||||
- `OCORE/Secrets/Cache/DpapiSecretsCache.cs` — DPAPI encrypt/decrypt of the
|
||||
local cache file.
|
||||
- `OCORE_web/Secrets/SecretManagementWebExtensions.cs` — the
|
||||
`WebApplicationBuilder.AddSecretManagement()` entry point Fuchs calls.
|
||||
- `OCORE_web/.github_instructions/secretsmanagement.instructions.md` —
|
||||
canonical mechanism reference (naming/encoding rules, cache format,
|
||||
"adding a new secret" steps), kept current independently of this doc.
|
||||
- `.github/copilot-instructions.md` ("Azure Key Vault — Secret Naming") —
|
||||
the naming rules summarized for this repo, with the `fuchs--` prefix.
|
||||
|
||||
## Related decisions
|
||||
- [0031 — Shared Key Vault + DPAPI secret management via OCORE/OCORE_web](../Decisions/0031-shared-keyvault-dpapi-secret-management.md)
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
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.
|
||||
Reference in New Issue
Block a user