diff --git a/docs/GatewayConfiguration.md b/docs/GatewayConfiguration.md index 9ed343b..e26406a 100644 --- a/docs/GatewayConfiguration.md +++ b/docs/GatewayConfiguration.md @@ -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 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= +``` + +`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= +set Secrets__SqlitePath=C:\ProgramData\MxGateway\mxgateway-secrets.db +secret set ldap/mxgateway/bind +``` + +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 | Option | Default | Description |