docs(secrets): G-5 master-key operator note (Environment provider via NSSM env)

This commit is contained in:
Joseph Doherty
2026-07-16 11:13:16 -04:00
parent 22a878c36e
commit 2e7245dd55
+82
View File
@@ -255,6 +255,88 @@ When LDAP is enabled, `Server`, `SearchBase`, `ServiceAccountDn`,
must be in range. See `glauth.md` for the shared dev instance and the must be in range. See `glauth.md` for the shared dev instance and the
dev→production hardening posture. dev→production hardening posture.
## Secrets Master Key
`${secret:...}` tokens in configuration — currently just
`MxGateway:Ldap:ServiceAccountPassword` (see above) — are resolved at startup
from an encrypted local SQLite store, bound from the top-level `Secrets`
section (a sibling of `MxGateway`, not nested under it):
| Option | Default | Description |
|--------|---------|-------------|
| `Secrets:SqlitePath` | `mxgateway-secrets.db` | Path to the encrypted secrets store, resolved relative to the app content root when not rooted. |
| `Secrets:MasterKey:Source` | `Environment` | Key-encryption-key (KEK) provider. `Environment` reads a base64-encoded 32-byte key from an env var; `Dpapi` uses a machine-bound key file instead (see below). |
| `Secrets:MasterKey:EnvVarName` | `ZB_SECRETS_MASTER_KEY` | Env var name the `Environment` provider reads the KEK from. |
The gateway always opens (and, if needed, creates and schema-migrates) this
store at startup, even when zero `${secret:}` tokens are configured.
### Delivering the master key on the NSSM box
MxAccessGateway is a single Windows/NSSM box with no clustering, so the
simplest posture applies: the `Environment` provider delivers
`ZB_SECRETS_MASTER_KEY` via the NSSM service environment — the same mechanism
already used for `MxGateway__Ldap__ServiceAccountPassword` overrides and the
other `AppEnvironmentExtra` entries (see `wonder-app-vd03`/`windev` deploy
notes). The key is **never committed** to `appsettings.json` or the repo.
```text
nssm get MxAccessGw AppEnvironmentExtra
nssm set MxAccessGw AppEnvironmentExtra ZB_SECRETS_MASTER_KEY=<base64-32-bytes>
```
`AppEnvironmentExtra` is the same newline-separated `KEY=VALUE` block that
already carries `ASPNETCORE_ENVIRONMENT`, `MxGateway__Worker__ExecutablePath`,
and the API-key pepper (see the `wonder-app-vd03`/`windev` deploy notes) —
`nssm set ... AppEnvironmentExtra` **replaces the entire block**, so always
`get` first and re-include every existing entry alongside the new key, then
restart the service for it to take effect.
### Seeding a secret
Once the master key is in place, seed (or update) a secret on the box with
the `secret` CLI (`ZB.MOM.WW.Secrets.Cli`). The CLI is config-driven — it
reads `Secrets:SqlitePath` and the master-key settings from its own
`appsettings.json` plus environment variables, exactly like the gateway — so
run it with the same `ZB_SECRETS_MASTER_KEY` set and, if it isn't colocated
with a config that already points at the gateway's store, an explicit
`Secrets__SqlitePath` override so both processes read/write the same file:
```text
set ZB_SECRETS_MASTER_KEY=<base64-32-bytes>
set Secrets__SqlitePath=C:\ProgramData\MxGateway\mxgateway-secrets.db
secret set ldap/mxgateway/bind <the real LDAP bind password>
```
A mismatched `Secrets:SqlitePath` between the CLI and the gateway silently
seeds a different store, and the gateway then fails closed at startup as if
the secret were never set.
### Fail-closed
If a referenced `${secret:...}` token cannot be resolved — the secret is
missing from the store, or `ZB_SECRETS_MASTER_KEY` is absent or wrong — the
gateway refuses to start (`SecretNotFoundException`, or a master-key error
raised before that). There is no fallback to a blank or leaked credential.
### Alternative: DPAPI
`Secrets:MasterKey:Source = Dpapi` uses a machine-bound key file instead of an
env var — stronger at-rest binding (the key never appears in the service
environment or process listing) at the cost of tying the store to one
machine, which complicates DR restore (the key file does not travel with a
backup/restore of the `.db` file alone). Not used on the current NSSM
deployments; documented here as the alternative posture if the env-var
exposure becomes a concern.
### Rotation / DR
Losing the KEK makes the store permanently unrecoverable — there is no
recovery path other than re-seeding every secret from scratch. Back the KEK
up in the org secret vault, not just on the box. Full KEK rotation
(re-wrapping every secret under a new key, `RewrapAll`) is a future item and
is **not yet available**.
## Protocol Options ## Protocol Options
| Option | Default | Description | | Option | Default | Description |