docs(env2): live-gate docker-env2 on the gRPC PSK build — PASS 3/3 (Gitea #31)

Rebuilt scadabridge:latest from main @ 8524a7f7 and recreated only the
env2 containers. All three gate checks pass on site-x:
1. both site nodes boot with the key (StartupValidator fail-closed →
   reaching 'Application started' proves the key present);
2. control-plane PSK auth: no-header / wrong-key ⇒ PermissionDenied,
   correct key ⇒ success, on both nodes (:9123, :9124);
3. LocalDb unaffected (local-only; 0 errors, healthy boot).
Bonus: central registers site-x online via gRPC heartbeat; no real
ClusterClient/receptionist (only the benign ClusterClientSiteAuditClient
label, same as the primary rig). Noted a seed-data gap (ScadaBridgeConfig2
dbo.Sites is empty) — orthogonal to the transport.
This commit is contained in:
Joseph Doherty
2026-07-23 14:10:22 -04:00
parent 8524a7f746
commit e0f105c3b3
@@ -475,5 +475,68 @@ default (Phase 4 flipped the shipped appsettings to `CentralGrpcEndpoints` — u
redeploy from `main` now yields all-gRPC, not all-Akka). No ClusterClient remains anywhere in the
running system or the source tree. Deferred, unchanged from earlier phases: the instance-dependent
central→site RPC matrix (`ExecuteOpcUa`/`ExecuteRoute`/standby parked retry) and live
`TriggerFailover`, all unit-proven; PSK **rotation** on a live pair; `docker-env2` was updated with
its own key but not gated here.
`TriggerFailover`, all unit-proven; PSK **rotation** on a live pair. `docker-env2` is now gated —
see below.
---
## Env2 gate — `docker-env2` on the gRPC PSK build — **PASS** (2026-07-23, Gitea #31)
The secondary Transport-testing topology (`docker-env2/`: 2 central + 1 site-x × 2 nodes, host
ports 91XX, shared infra) carried its `GrpcPsk` in config since Phase 0 but had never been
redeployed or gated against the migration build. Rebuilt `scadabridge:latest` from `main` @
`8524a7f7` (includes the #28 Transport execution-strategy fix) and recreated only the env2
containers (`bash docker-env2/deploy.sh`; the primary rig's running image was untouched). Site-x
PSK `dev-grpc-psk-docker-env2-site-x` on both site nodes matches central's
`ScadaBridge__Communication__SitePsks__site-x`.
| # | Check | Result |
|---|-------|--------|
| 1 | Both site nodes boot with the key (StartupValidator passes) | **PASS** |
| 2 | Unauthenticated control-plane call ⇒ PermissionDenied; correct key ⇒ success | **PASS** |
| 3 | LocalDb unaffected | **PASS** |
### Check 1 — both site nodes boot
`scadabridge-env2-site-x-a` and `-b` both reached `Application started` with `Now listening on:
http://[::]:8083` (the gRPC control/data listener). `StartupValidator` is fail-closed on a missing
`ScadaBridge:Communication:GrpcPsk` — a site node without it refuses to boot — so reaching
`Application started` is itself the proof the key is present and valid. Each node also logged a clean
`Reconcile pass for site site-x node node-{a,b} complete: 0 fetched, 0 failed, 0 orphan(s)`, i.e.
the site→central gRPC path was already carrying traffic.
### Check 2 — PSK auth on the control plane
`grpcurl` against the site-hosted `SiteStreamService/PullAuditEvents` on both nodes
(`localhost:9123` = site-x-a, `localhost:9124` = site-x-b):
- **No auth header** ⇒ `PermissionDenied: Control plane authentication failed.`
- **Wrong bearer key** (`Bearer WRONG-KEY`, `x-scadabridge-site: site-x`) ⇒ `PermissionDenied`.
- **Correct key** (`Bearer dev-grpc-psk-docker-env2-site-x`) ⇒ `{}` (empty audit set — the env has
no S&F driver seeded — but **not** a permission error: auth accepted). Identical on both nodes.
(Same contract clarification as the primary rig: the site interceptor authenticates the node's single
`GrpcPsk` and does not require `x-scadabridge-site`; per-site isolation is proven by the wrong-key
rejection.)
### Check 3 — LocalDb unaffected
Env2 site-x runs LocalDb **local-only** (no `LocalDb:Replication:PeerAddress` configured — the rig's
replication posture is proven on the primary `docker/` stack, not here), so "unaffected" means the
consolidated site DB stays operational on the gRPC build. Site logs hold **0** LocalDb
errors/exceptions and both nodes boot healthy with the LocalDb at `/app/data/site-localdb.db`; the
clean reconcile passes above confirm the site DB + site→central path work end to end.
### Bonus / observations
- **No real ClusterClient.** Central logs hold **0** ClusterClient/receptionist references; the site
logs' only match is the benign string label `client=ClusterClientSiteAuditClient` in a
`SiteAuditTelemetryActor created` line (a legacy identifier, **not** an Akka `ClusterClient`) —
identical to the primary rig's site nodes. No real receptionist/ClusterClient actor path exists.
- **Central registers site-x over gRPC.** `Site site-x registered online via heartbeat` — the
site→central gRPC command/control path is live.
- **Seed-data note (not a migration issue).** `ScadaBridgeConfig2.dbo.Sites` has **0 rows**, so
central logs `Reconcile request from unknown site 'site-x' … replying with empty gap` and evicts
it from the health aggregator as "no longer configured." This is a first-time-setup seeding gap
(`docker-env2/seed-sites.sh` not yet run on the fresh DB), orthogonal to the transport — which
demonstrably carries the heartbeat and reconcile regardless.