Secrets: production replication topologies decided — OtOpcUa Akka pair-local; ScadaBridge needs a central gRPC hub replicator (pull-only) #3
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes the previously-untracked production-enablement decision for
ZB.MOM.WW.Secretsclustered replication (flagSecrets:Replication:Enabled, default off everywhere; live-validated only on dev rigs with the committed dev KEK).Decisions (owner, 2026-08-07)
/admin/secrets, site pairs via thesecretCLI or ciphertext-only bundle import).ZB.MOM.WW.Secrets.Replicator.Grpc: central hosts asecrets_hub.v1gRPC service backed by its own store; site nodes run the existing reconciler sweep against it over the established central↔site gRPC plane (bearer auth, fail-closed). Pull-only by design — updates originate at central only (owner-confirmed invariant, matching how all site-bound configuration flows). Offline posture unchanged from the hub mode: a site booting with central unreachable serves last-known-good local secrets and converges on the first successful sweep.Work items
591307c)docs/plans/2026-08-07-secrets-production-topology-and-grpc-hub.md(this repo)591307c) add a Production enablement section toZB.MOM.WW.Secrets/docs/operations/clustered-secrets.mdZB.MOM.WW.Secrets.Replicator.Grpc(proto + central hub service + pull-only sweep client), offline suite — (shipped0.4.0@321ab22,0.4.1@c86cead; 6 packages on the feed, restore-verified. Pull-only enforced at the contract: exactly two RPCs,GetManifest/GetSecrets, no write RPC — pinned by a descriptor-reflection test. Fail-closed constant-time bearer auth, uniform denial,GetSecretsbatch cap. Grpc suite 166 tests; solution 444/0/15.ZB.MOM.WW.Secrets/CHANGELOG.mdcreated.)ScadaBridge main @ f6c3f7c5, fast-forward merged + pushed. Gate PASS 4/4,ScadaBridge/docs/plans/2026-08-07-secrets-grpc-hub-live-gate.md: convergence 5 s byte-identical + decrypt-verified on both site-a nodes; cold boot with central down serves last-known-good and resumes (14 s / 43 s); tombstone <=9 s, no resurrection; auth negatives uniform, 0.4.1 denial Warning fired, rate limit proven defer-not-drop, zero credential material in any log. Role is a method parameter (SecretsNodeRole.Central|Site) so config cannot flip a site into hosting the hub; mode keySecrets:Replication:ModedefaultSqlServer; hub on central's existing h2c listenerCentralGrpcPort8083; 31 DI/endpoint pins. Flag default-off; rig enables central pair + site-a only.)lmxopcua master @ 1bd79c8e. PASS 4/4,OtOpcUa/docs/plans/2026-08-07-secrets-pair-local-live-gate.md: pair convergence <=7 s; intra-pair delete <1 s; clean pair restart; no cross-pair propagation proven with seeded isolation controls on the other pairs, not by mere absence. Supersedes the 2026-07-18 six-node gate as evidence. Rig fix landed with it:Secrets__SqlitePathmoved onto each node's per-node/app/datavolume. Stale appsettings_commentclaiming the feature is non-functional corrected inlmxopcua master @ 01256344.)rewrap-allfan-out under both topologies (re-wraps deliberately do not replicate) — (ZB.MOM.WW.Secrets/docs/operations/clustered-secrets.md, Production-enablement section. OtOpcUa = both nodes of every pair. ScadaBridge = both central nodes plus both nodes of every site pair, because each central node has its own independent store — see the residual below.)Side benefit (not in scope)
A ciphertext-only central->site secrets channel is what the permanently-unreplicated
notification_lists/smtp_configurationstables have been waiting for: an SMTP password stored as a${secret:}ref rides this channel as ciphertext, removing the plaintext-password objection that blocked replicating that config.Status 2026-08-07: all work items done; closing. One residual is carried forward as scadaproj#4 — ScadaBridge's central pair does not converge with itself and the site client dials a single endpoint, which are one design question.
Secrets:Replication:Enabledremains default-off in both products; production enablement is a separate operator action, and for ScadaBridge it should wait on #4.Closing — all four work items delivered and live-gated
Package.
ZB.MOM.WW.Secrets.Replicator.Grpcshipped in family0.4.0(scadaprojmain @ 321ab22), then0.4.1(c86cead) adding a rate-limited denial Warning that the ScadaBridge live gate asked for. 6 packages on thedohertj2-giteafeed, restore-verified.ZB.MOM.WW.Secrets/CHANGELOG.mdcreated. Grpc package suite 166 tests; solution 444 pass / 0 fail / 15 skipped.Decision 4's pull-only invariant is enforced at the contract, not by convention:
secrets_hub.v1declares exactly two RPCs —GetManifestandGetSecrets— and there is no write RPC to call. Pinned by a descriptor-reflection test in the package and a route pin on the ScadaBridge side. Auth is fail-closed constant-time bearer with uniform denial (no oracle), a single public constructor so the auth-less shape is unconstructable, and aGetSecretsbatch cap answeringResourceExhausted. The site sweep is never-throw with per-row parse isolation, so one malformed row cannot poison a sweep and offline stays last-known-good.ScadaBridge —
main @ f6c3f7c5, gate PASS 4/4 (docs/plans/2026-08-07-secrets-grpc-hub-live-gate.md). Convergence 5 s, byte-identical and decrypt-verified on both site-a nodes; cold boot with central down serves last-known-good and resumes (14 s / 43 s); tombstone ≤9 s with no resurrection; auth negatives uniform, the denial Warning fires immediately, the rate limit proven to defer, not drop, and zero credential material in any log — including the rejected tokens themselves. Wiring detail worth recording: the node role is a method parameter (SecretsNodeRole.Central|Site), not a config key, so no configuration change can flip a site node into hosting the hub; the mode keySecrets:Replication:Modedefaults toSqlServer(legacy-equivalent); the hub is mapped on central's existing h2c control-plane listener (CentralGrpcPort, default 8083) rather than a new port; startup fails closed on a missing bearer token or endpoint. 31 DI/endpoint pin tests.OtOpcUa —
master @ 1bd79c8e, gate PASS 4/4 (docs/plans/2026-08-07-secrets-pair-local-live-gate.md). Pair convergence ≤7 s; intra-pair delete <1 s; clean pair restart; and no cross-pair propagation proven with seeded isolation controls on the other pairs, rather than inferred from a row's absence. This supersedes the 2026-07-18 six-node gate as evidence — that gate predates the mesh Phase 6 partition, and Phase 6's own secrets leg was inference-only, with no secret ever seeded. A rig defect landed with it:Secrets__SqlitePathmoved onto each node's per-node/app/datavolume. Separately, the OtOpcUaappsettings.jsonSecrets:Replicationblock still warned that the feature was "KNOWN NON-FUNCTIONAL … do not enable until the library is fixed" — false since0.2.2— corrected inlmxopcua master @ 01256344.Docs. Runbook
ZB.MOM.WW.Secrets/docs/operations/clustered-secrets.md(Production enablement + rotation fan-out),components/secrets/GAPS.md, and the scadaproj index row all updated.Both products remain default-off. Production enablement is a separate operator action; nothing here turns replication on anywhere but the dev rigs.
Residual → scadaproj#4. ScadaBridge's central pair does not converge with itself — each central node has an independent local store, and
central-bwas live-observed answering an authenticatedGetManifestwith an empty manifest for the whole gate run whilecentral-aheld every secret. Combined with the client dialing a singleEndpoint, that makes central-node authority and endpoint failover one design question rather than two: failover to the second central would succeed against an empty hub — not data loss, since pull-only never deletes, but a silent convergence stop. ScadaBridge production enablement should wait on #4. Two minor notes are folded into the same issue: delete-while-follower-offline is untested, and alerting on the denial Warning must sum per-window deltas because the warning is deliberately rate-limited.Closing.