Secrets gRPC hub: central-pair convergence + client endpoint failover are one design question #4
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?
Found during the ScadaBridge gRPC-hub live gate (
ScadaBridge/docs/plans/2026-08-07-secrets-grpc-hub-live-gate.md, PASS 4/4,ScadaBridge main @ f6c3f7c5). Neither observation is a gate failure — the gate exercised the central-a hub only, which is the topology the rig enables — but together they leave one unanswered design question that must be settled before production enablement.Follow-on to scadaproj#3; design at
docs/plans/2026-08-07-secrets-production-topology-and-grpc-hub.md.Observed (live, verified)
1. The central pair does not converge with itself. Each central node has an independent local secret store, and nothing replicates between them. During the gate,
central-banswered an authenticatedGetManifestwith an empty manifest for the entire run, whilecentral-aheld every secret. The hub is mapped on both central nodes (it is wired to the central role, not to a leader), so a healthy-looking, correctly-authenticating hub can serve nothing.2. The site client dials a single
Endpoint.GrpcSecretSyncServicetargets one configuredSecrets:GrpcHub:Endpointwith no failover to the second central node.Why these are one question, not two
Adding endpoint failover on its own makes the problem worse, not better: a client that failed over from
central-atocentral-btoday would succeed — a valid TLS/h2c connection, a valid bearer token, a 200 response — and receive an empty manifest. Because the channel is pull-only and never deletes, that is not data loss; sites keep serving last-known-good. It is a silent convergence stop: sites stop receiving new and rotated secrets with no error anywhere, and secrets authored on the node the sites do not dial reach nobody at all.So "which central node is authoritative for secrets" and "what does a site do when that node is unreachable" have to be answered together. Sketch of the option space (not a decision):
Whichever is chosen, the fail-closed rule should extend to this case: a hub that is not authoritative must refuse, never answer empty. An empty manifest is currently indistinguishable from "central legitimately has no secrets."
Minor notes from the same gate (fold in here rather than filing separately)
Not affected
OtOpcUa/docs/plans/2026-08-07-secrets-pair-local-live-gate.md, PASS 4/4,master @ 1bd79c8e).The
Secrets:Replication:Enabledflag remains default-off in both products; production enablement is a separate operator action and should not happen for ScadaBridge until this is settled.RESOLVED — shipped as the central-shared-SQL-store program (plan
scadaproj/docs/plans/2026-08-07-secrets-central-shared-sql-store.md, decision record user-approved 2026-08-07).Decision
Central-pair convergence/replication was rejected in favour of moving both ScadaBridge central nodes'
ISecretStoreonto ONE shared SQL Server store (AddZbSecretsSqlServerStore, the SqlServer replicator's shared-store mode) inSecrets:Replication:Mode=Grpc. Divergence is removed structurally — there is one copy of every row, so both hub instances serve identical manifests by construction (no replication-lag window), endpoint failover becomes trivially safe (either central answers with the truth), and central'srewrap-allcollapses to one run. Accepted trade: central secret resolution now depends on SQL Server availability — acceptable because central is already SQL-coupled, and a SQL outage makes the hub throw (sites keep last-known-good), never serve empty.Shipped
0.5.0(scadaprojmain @ 7a2980efailover reader →e62b4a5release, 6 packages on the feed, restore-verified):Secrets:GrpcHub:FallbackEndpoints+FailoverSecretsHubReader— ordered fallback with sticky preference,RpcException-only failover, same-store-only rule documented at the option (listing independently-stored endpoints is exactly this issue's silent-convergence-stop).0.5.1(scadaprojmain @ 31ca940):SqlServerSecretsStoreMigratorvirgin-DB concurrent bring-up race fixed — retry filter now covers 1205/2714/1913/2627 (found live: 0.5.0 crashed central-a on the pair's simultaneous first boot against an empty DB).main @ 68f812ea→4d7f09d5gate doc/rig →e697477c0.5.1 pins, all pushed): central+Grpc now REQUIRESSecrets:SqlServer:ConnectionString(fail-closed via sharedEnsureCentralSharedStoreConnectionString, refusing blank and${secret:}values), Layer-A pre-host expander follows the store swap, sites unchanged,FallbackEndpointswired; 472 offline tests.Live gate — PASS 5/5 + re-drill
ScadaBridge/docs/plans/2026-08-07-secrets-central-shared-store-live-gate.md:GetSecrets), one SQL row; this issue's empty-manifest-from-central-b defect is unreproducible (central-b has no store of its own to be empty).${secret:}resolution reads the SHARED store.Re-drill on 0.5.1: virgin-DB simultaneous central bring-up clean (both booted in the same second, schema provisioned once) where 0.5.0 crashed reproducibly.
Residuals
running, restart policy never fires). Pre-existing (affects the old SQLite path too), reproduced twice during this gate.Runbook (
ZB.MOM.WW.Secrets/docs/operations/clustered-secrets.md) updated: ScadaBridge rewrap fan-out is now ONE central run against the shared SQL store + both nodes of every site pair; the "both central nodes" caveat this issue created is deleted. ScadaBridge production enablement no longer waits on this issue.