docs(secrets): G-5 master-key operator note (Environment provider via NSSM env)
This commit is contained in:
@@ -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 |
|
||||||
|
|||||||
Reference in New Issue
Block a user