docs(plans): Batch B close-out — Phase 1A security register, issue queue, tracking

This commit is contained in:
Joseph Doherty
2026-08-06 04:24:45 -04:00
parent a463141028
commit c2bfad4af6
2 changed files with 37 additions and 3 deletions
@@ -22,7 +22,7 @@ posture).
| Phase | Scope | Branch | Status | PR / merge SHA | Live gate |
|---|---|---|---|---|---|
| 0 | Proto + mappers + goldens; both auth interceptors; `IMeshPskProvider`; options/validators (+`Grpc` value, SplitTopology relaxation); rig dark config; stale-comment fixes; FleetStatus issue filed | `feat/grpc-phase0-contract-auth` | **IN PROGRESS** — Batch A (0.1 + 0.3) done @ `aa21f61f` + close-out `09db9e47`; 0.2 (mapper + 27 wire-level goldens) done @ `545413a8`; all reviews spec ✅ + code ✅, 0 critical/important; Batch B (0.4 + 0.5) next | — | PENDING |
| 0 | Proto + mappers + goldens; both auth interceptors; `IMeshPskProvider`; options/validators (+`Grpc` value, SplitTopology relaxation); rig dark config; stale-comment fixes; FleetStatus issue filed | `feat/grpc-phase0-contract-auth` | **IN PROGRESS** — Batch A (0.1 + 0.3) @ `aa21f61f`; 0.2 (mapper + 27 wire-level goldens) @ `545413a8`; Batch B (0.4 + 0.5) @ `fbda015c`/`68a416e4` + review fixes `24e55d33`/`a4631410`; all reviews spec ✅ + code ✅ (Batch B code review on Opus, fix loop closed); only 0.6 remains | — | PENDING |
| 1A | Central→node command plane (`NodeCommandService` on the Telemetry listener; `MeshGrpcCommandDialer`; third mode branch in `CentralCommunicationActor`) — **worktree A** | `feat/grpc-node-command` | NOT STARTED | — | PENDING |
| 1B | Node→central ack plane (`CentralCommandService` on the ConfigServe listener; `GrpcApplyAckSender`; third branch in `NodeCommunicationActor`) — **worktree B**, rebases on 1A | `feat/grpc-central-ack` | NOT STARTED | — | PENDING |
| 2 | Baseline reproduction on pre-flip build, then full-rig cutover (`OTOPCUA_MESH_MODE` default → `Grpc`) + soak + kill drills | (rig commit) | NOT STARTED | — | PENDING |
@@ -48,6 +48,39 @@ phase DoD — PR + merge are user-owned.**
| 2026-08-06 | `AlarmCommandMessage.operation` is a proto `string`, NOT an enum — the CLR `AlarmCommand.Operation` is an unconstrained string, and an enum would silently decode unknown operations to `UNSPECIFIED` (the drop class this migration removes). Plan T0.1's "AlarmCommand verb" enum wording was a plan defect; only `ApplyOutcome` carries the enum-0 rule. T0.2's goldens must pin pass-through of an unrecognized operation string | proto comment + this log (T0.1 review) |
| 2026-08-06 | `correlation_id` lives on the envelope AND as inner fields on the three commands whose CLR record carries one — each command round-trips independently in the T0.2 mapper; the envelope covers `AlarmCommand` (no CLR correlation member). For the three, the inner field is authoritative on decode | proto comment + this log (T0.1 review) |
## Phase 1A security register (from the Batch B Opus code review — HARD GATE: all four close BEFORE `CentralCommandService` is mapped)
1. **Known-cluster-set membership gate in `TryGetPsk`** — central rebuilds its cluster set from
`ClusterNode` rows every `ContactRefreshSeconds`; refuse a cluster id outside that set in
O(1) with no store touch. Closes BOTH open review findings at once: (a) the timing oracle
that currently defeats the uniform-denial anti-enumeration text (unknown cluster pays a
store read on every probe because negatives are deliberately never cached; known cluster is
cached — a stable remotely-measurable distinguisher), and (b) the pre-auth resource
exhaustion / audit-flood vector (each distinct forged `x-otopcua-cluster` header costs a
secret-store read + a `secret.resolve.not-found` audit row into `ConfigAuditLog` via the
audit singleton, attacker-chosen `Target` values, before any credential check). Namespace
confinement (`OT-MESH-PSK-` prefix + `SecretName` allow-list) means no disclosure risk —
this is flood/oracle only.
2. **`bool Matches(string clusterId, string presented)` on `IMeshPskProvider`** — keep
`FixedTimeEquals` inside the provider so a future verify-side caller cannot write `==` and
have it compile. Do before 1B adds the dial-side caller.
3. **DI-shape pin test** — `IMeshPskProvider` is `hasAdmin`-only and its pairing with the
central interceptor is by convention; a driver-only node injecting it throws on first call,
not at boot. Add a Host.IntegrationTests check (admin host resolves it; driver-only host
does NOT) when Phase 1 maps the services.
4. **Integration gate over real Kestrel h2c** — both new interceptors have unit coverage only;
the one-public-ctor reflection test approximates but cannot prove the interceptor is
actually invoked. Wire-probe in the 1A/1B live gates (already in the live-gate tables).
Non-gating notes from the same review: central-side unseeded-PSK is invisible until a deploy
stalls (consider a "PSK seeded for every known cluster?" `/hosts`/health surface — the
#521/#541 shape); `SecretName` lowercases + bans `_`, so a cluster id like `Site_A` can never
resolve from the store and must use `ClusterPsks` — MUST be documented in T0.6's
`docs/Configuration.md` stub (rig ids `MAIN`/`SITE-A`/`SITE-B` are fine); sibling interceptors
still use `IsNullOrEmpty` (whitespace key opens them) — family-wide backport is out of this
program's scope, file alongside the FleetStatus issue in T0.6; consider a non-throwing counter
metric if the poison-guard silent catch ever fires in production.
## Phase 1 dispatch notes (from Task 0.2 reviews — fold into the 1A/1B implementer prompts)
- The alarm arm of `FromEnvelope` necessarily discards the envelope correlation
@@ -84,6 +117,7 @@ Checks a gate could not drive, named + carried to the next gate (proportionate-g
| Issue | What | Status |
|---|---|---|
| *(Gitea # TBD in Phase 0, T0.6)* | `FleetStatusBroadcaster` derives "fleet" from central's own gossip ring — silently central-pair-blind since mesh Phase 6; pre-existing, transport-independent, out of this program's scope | to file |
| *(Gitea # TBD in Phase 0, T0.6)* | The three pre-existing auth interceptors (`TelemetryStream`/`ConfigServe`/`LocalDbSync`) treat a whitespace-only key as SET (`IsNullOrEmpty`), so a whitespace `Telemetry:ApiKey` etc. opens the endpoint to a whitespace bearer; the two new mesh interceptors use `IsNullOrWhiteSpace`. Backport is a behaviour change to shipped endpoints — own issue, out of this program's scope | to file |
## Known constraints carried from the research (do not rediscover)
@@ -6,8 +6,8 @@
{"id": 0, "subject": "Task 0.1: mesh_transport.proto + contract lock", "phase": "0", "branch": "feat/grpc-phase0-contract-auth", "classification": "standard", "parallelizableWith": ["0.3 (batch, same agent)"], "status": "completed", "commit": "c0ccfa27", "review": "spec ✅ + code ✅"},
{"id": 1, "subject": "Task 0.2: DTO mapper + round-trip goldens", "phase": "0", "branch": "feat/grpc-phase0-contract-auth", "classification": "high-risk", "parallelizableWith": [], "status": "completed", "blockedBy": [0], "commit": "545413a8", "review": "spec ✅ + code ✅ (serial)"},
{"id": 2, "subject": "Task 0.3: MeshTransportOptions + validators + SplitTopology relaxation", "phase": "0", "branch": "feat/grpc-phase0-contract-auth", "classification": "standard", "parallelizableWith": ["0.1 (batch, same agent)"], "status": "completed", "commit": "aa21f61f", "review": "spec ✅ + code ✅"},
{"id": 3, "subject": "Task 0.4: node-side MeshCommandAuthInterceptor", "phase": "0", "branch": "feat/grpc-phase0-contract-auth", "classification": "high-risk", "parallelizableWith": ["0.5 (batch, same agent)"], "status": "pending", "blockedBy": [2]},
{"id": 4, "subject": "Task 0.5: IMeshPskProvider + CentralCommandAuthInterceptor", "phase": "0", "branch": "feat/grpc-phase0-contract-auth", "classification": "high-risk", "parallelizableWith": ["0.4 (batch, same agent)"], "status": "pending", "blockedBy": [2]},
{"id": 3, "subject": "Task 0.4: node-side MeshCommandAuthInterceptor", "phase": "0", "branch": "feat/grpc-phase0-contract-auth", "classification": "high-risk", "parallelizableWith": ["0.5 (batch, same agent)"], "status": "completed", "blockedBy": [2], "commit": "fbda015c + fixes 24e55d33/a4631410", "review": "spec ✅ + code ✅ (opus, fix loop closed)"},
{"id": 4, "subject": "Task 0.5: IMeshPskProvider + CentralCommandAuthInterceptor", "phase": "0", "branch": "feat/grpc-phase0-contract-auth", "classification": "high-risk", "parallelizableWith": ["0.4 (batch, same agent)"], "status": "completed", "blockedBy": [2], "commit": "68a416e4 + fixes 24e55d33/a4631410", "review": "spec ✅ + code ✅ (opus, fix loop closed); 4 items gated on Phase 1A — see tracking §Phase 1A security register"},
{"id": 5, "subject": "Task 0.6: rig config + FleetStatus issue + docs stub + Phase 0 gate", "phase": "0", "branch": "feat/grpc-phase0-contract-auth", "classification": "small", "parallelizableWith": [], "status": "pending", "blockedBy": [0, 1, 2, 3, 4]},
{"id": 6, "subject": "Task 1A.1: NodeCommandGrpcService on the telemetry listener", "phase": "1A", "branch": "feat/grpc-node-command", "classification": "standard", "parallelizableWith": ["1B.1 (other worktree)"], "status": "pending", "blockedBy": [5]},
{"id": 7, "subject": "Task 1A.2: MeshGrpcCommandDialer + third branch in CentralCommunicationActor", "phase": "1A", "branch": "feat/grpc-node-command", "classification": "high-risk", "parallelizableWith": ["1B.2 (other worktree)"], "status": "pending", "blockedBy": [6]},