docs(secrets): document the plain-IConfigurationRoot reload gap and OtOpcUa's redundant starter (scadaproj#49)

The reload gap for a plain IConfigurationRoot is documented rather than fixed: closing it would mean
inserting a provider into ConfigurationRoot's internal list. No family host is exposed (plain roots
are built with reloadOnChange: false; the rest pass a ConfigurationManager). clustered-secrets.md
notes that AddZbSecretsAkkaReplication now starts the actor itself, so OtOpcUa's
SecretReplicationStarter becomes redundant once it adopts 0.6.6.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 15:23:37 -05:00
parent fac3c5355c
commit db441d9d99
2 changed files with 17 additions and 0 deletions
+12
View File
@@ -105,6 +105,18 @@ keeps its startup value until restart — editing it, or rotating the secret, ta
start. A plain `ConfigurationBuilder().Build()` root gets the values re-applied after each reload,
which is weaker; build such a root without `reloadOnChange`.
*Known gap, not fixed (scadaproj#49):* a plain `IConfigurationRoot` has a fixed provider list, so
the overlay cannot be added to it; the library writes the values into its providers and re-applies
them from the root's reload token. Direct `IConfiguration` reads see the pinned value after a reload,
but an `IOptionsMonitor` bound to that root can rebind from the placeholder first — reload callbacks
run in an order the library does not control (the token's callbacks run last-registered-first, and
the expander registers early). Inserting a provider into a built root would mean mutating
`ConfigurationRoot`'s internal list, which this library deliberately does not do. Nobody in the
family is exposed today (ScadaBridge and the ignitionoee router build their plain root with
`reloadOnChange: false`; the other hosts pass a `ConfigurationManager`). If you must reload a plain
root, expand into a `ConfigurationManager` instead, or re-read the key per use rather than through
`IOptionsMonitor`.
The master key is 32 bytes, provided base64 in `ZB_SECRETS_MASTER_KEY` (Environment source).
A missing/invalid key fails closed at startup (`MasterKeyUnavailableException`).
@@ -154,6 +154,11 @@ Optional but recommended: without it the protocol still round-trips under Akka's
serializer, but the serializer carrying secret ciphertext becomes an inherited default rather than
a deliberate choice.
Since `0.6.6` `AddZbSecretsAkkaReplication` starts the replication actor itself once the host has
started (scadaproj#33), so a node that only expands `${secret:}` references pre-host still
replicates. **OtOpcUa's own `SecretReplicationStarter` is then redundant** — it resolves the same
actor, so keeping it is harmless; drop it when OtOpcUa adopts `0.6.6` (scadaproj#49).
### Securing the transport
Rows on the wire are ciphertext and the KEK never travels, so a passive eavesdropper learns