docs(secrets): G-4 ${secret:} config delivery convention + fail-closed proof (Task 4)
OtOpcUa commits no plaintext secrets — the only secret-shaped committed value is
ServerHistorian:ApiKey='' (empty, disabled by default). Because the pre-host expander
is fail-closed AND section-agnostic, hard-committing a ${secret:} token would break
every dev/CI/integration boot for zero at-rest benefit. Instead document the token
delivery convention for all five config secrets (jwt/ldap/deploy/configdb/historian)
in docs/security.md and note the token option in the ServerHistorian comments.
Mechanism proven live: unseeded token boot fails with SecretNotFoundException at
pre-host expansion; seeded token resolves and boot proceeds. Behavior-neutral
(comment + doc only).
This commit is contained in:
@@ -380,6 +380,51 @@ polling the node.
|
||||
|
||||
---
|
||||
|
||||
## Config secrets (`${secret:}` delivery)
|
||||
|
||||
OtOpcUa never commits secret values to `appsettings*.json`. Every real secret is
|
||||
supplied at deploy time — either as a plain environment variable, or as a
|
||||
`${secret:<name>}` token backed by the shared **`ZB.MOM.WW.Secrets`** encrypted store.
|
||||
|
||||
A pre-host expander (`SecretReferenceExpander.ExpandConfigurationAsync`, wired in
|
||||
`OtOpcUa.Host/Program.cs`) walks the assembled configuration and rewrites every
|
||||
`${secret:<name>}` token into its resolved plaintext **before** the host is built —
|
||||
so every downstream binder/validator (`AddZbSerilog`, `AddOtOpcUaConfigDb`, the first
|
||||
`ValidateOnStart`) sees resolved values. The store's key-encryption key (KEK) comes from
|
||||
the `ZB_SECRETS_MASTER_KEY` environment variable (`Secrets:MasterKey:Source=Environment`);
|
||||
the encrypted SQLite store lives at `Secrets:SqlitePath`.
|
||||
|
||||
The expander is **fail-closed and section-agnostic**: a `${secret:<name>}` token whose
|
||||
secret is absent throws `SecretNotFoundException` at startup, regardless of which feature
|
||||
owns the key or whether that feature is enabled. Keys prefixed with `_` (comment keys) are
|
||||
skipped, so a `${secret:...}` example inside a `_…Comment` value is never resolved.
|
||||
|
||||
The five config secrets and their canonical secret names:
|
||||
|
||||
| Config key | Owner | Secret name |
|
||||
|---|---|---|
|
||||
| `Security:Jwt:SigningKey` | `JwtOptions` | `otopcua/jwt/signing-key` |
|
||||
| `Security:Ldap:ServiceAccountPassword` | `LdapOptions` | `otopcua/ldap/service-account-password` |
|
||||
| `Security:DeployApiKey` | `DeployApiEndpoints` | `otopcua/deploy/api-key` |
|
||||
| `ConnectionStrings:ConfigDb` | `AddOtOpcUaConfigDb` | `otopcua/sql/configdb-connstr` |
|
||||
| `ServerHistorian:ApiKey` | `ServerHistorianOptions` | `otopcua/historian/api-key` |
|
||||
|
||||
**Delivery.** By default these are delivered as plain environment variables (e.g.
|
||||
`ServerHistorian__ApiKey=histgw_…`), never committed. To deliver one from the encrypted
|
||||
store instead, seed it once with the `secret` CLI (from `ZB.MOM.WW.Secrets`), then supply
|
||||
the token in place of the literal — via env var or a deployment appsettings overlay:
|
||||
|
||||
```
|
||||
secret set otopcua/historian/api-key <value> # seed the encrypted store first
|
||||
ServerHistorian__ApiKey='${secret:otopcua/historian/api-key}'
|
||||
```
|
||||
|
||||
Only switch a value to a `${secret:}` token **after** the secret is seeded — an unseeded
|
||||
token fails the boot fail-closed. Do not commit the KEK (`ZB_SECRETS_MASTER_KEY`) or any
|
||||
secret value.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Certificate trust failure
|
||||
|
||||
Reference in New Issue
Block a user