fix(localdb): sync endpoint enforces the configured ApiKey (scadaproj#36)
LocalDbSyncService never read request headers, so setting LocalDb:Replication:ApiKey on both nodes left the passive endpoint open (LocalDb-001). It now requires Authorization: Bearer <key> (constant-time) before the handshake and before newest-wins takeover, refusing anything else with Unauthenticated. A blank key keeps the old accept-all behaviour and logs a startup warning. Wire-compatible: the initiator has always sent the header when the key is set; ScadaBridge/OtOpcUa interceptors use the same key. LocalDb 0.3.5 -> 0.3.6 (unpublished). Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
| Last reviewed | 2026-11-09 |
|
||||
| Reviewer | Claude |
|
||||
| Commit reviewed | `91fb2b4` |
|
||||
| Open findings | 7 |
|
||||
| Open findings | 6 |
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -73,7 +73,7 @@ and #26 (Microsoft.Data.Sqlite pool bump) are excluded.
|
||||
| Severity | Medium |
|
||||
| Priority | P2 |
|
||||
| Category | Security & secret handling |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
| Location | `ZB.MOM.WW.LocalDb/src/ZB.MOM.WW.LocalDb.Replication/LocalDbSyncService.cs:85`, `ZB.MOM.WW.LocalDb/src/ZB.MOM.WW.LocalDb.Replication/ReplicationOptions.cs:11`, `ZB.MOM.WW.LocalDb/src/ZB.MOM.WW.LocalDb.Replication/DependencyInjection/ReplicationServiceCollectionExtensions.cs:77` |
|
||||
|
||||
**Description**
|
||||
@@ -125,7 +125,23 @@ the `ApiKey` XML doc either way. Both consumers could then delete their copies.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
Resolved 2026-11-09 (scadaproj#36, LocalDb `0.3.6`, unpublished). Re-verified: at `91fb2b4` nothing in the library reads
|
||||
request headers. `LocalDbSyncService.Sync` now calls `IsAuthorized` before the takeover and before the handshake. When
|
||||
`ApiKey` is set, the stream must carry `Authorization: Bearer <key>`, compared with `CryptographicOperations.FixedTimeEquals`
|
||||
over UTF-8; anything else is refused with `Unauthenticated`. The check sits before the takeover, so an unauthenticated
|
||||
caller can neither read the Handshake (table names and digests) nor displace the real peer.
|
||||
|
||||
As the task requires, a blank `ApiKey` keeps the old behaviour (accept), and the service logs a startup warning. That
|
||||
rules out the review's fail-closed-by-default suggestion: a key-less pair must keep syncing. Wire compatibility holds,
|
||||
because the initiator has sent this header whenever the key is set since `0.1.0`. The ScadaBridge and OtOpcUa
|
||||
interceptors check the same key and the same header, so they are unaffected. The `ApiKey` XML doc, the README config
|
||||
table and the README Ops notes are corrected.
|
||||
|
||||
Tests (`GrpcAdapterTests`): two-node convergence with the key on both sides. Missing, wrong, scheme-less and
|
||||
over-long tokens are each refused before the handshake. A correct token gets the Handshake. An unauthenticated
|
||||
stream does not displace a live peer: there is no redial, and replication continues. A key on the initiator only
|
||||
still syncs, and a blank, empty or whitespace key admits every caller. With the check disabled, the 5 refusal and
|
||||
non-displacement cases fail.
|
||||
|
||||
### LocalDb-002 — Snapshot streams tables by name and rows by `pk_json` text, so FK children land before parents and are dead-lettered for good
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ code-reviews/
|
||||
|
||||
## Summary
|
||||
|
||||
10 of 10 libraries reviewed. 23 pending findings across all libraries.
|
||||
10 of 10 libraries reviewed. 22 pending findings across all libraries.
|
||||
|
||||
Coverage: Secrets, LocalDb, GalaxyRepository and Overview first reviewed, and Auth, Health,
|
||||
Theme, Audit, Configuration and Telemetry re-reviewed, on 2026-11-09 (#15). Each finding also
|
||||
@@ -50,9 +50,9 @@ carries a P1/P2/P3 priority (P1 = Critical/High, P2 = Medium, P3 = Low).
|
||||
|----------|---------------|
|
||||
| Critical | 0 |
|
||||
| High | 0 |
|
||||
| Medium | 4 |
|
||||
| Medium | 3 |
|
||||
| Low | 19 |
|
||||
| **Total** | **23** |
|
||||
| **Total** | **22** |
|
||||
|
||||
## Library Status
|
||||
|
||||
@@ -63,7 +63,7 @@ carries a P1/P2/P3 priority (P1 = Critical/High, P2 = Medium, P3 = Low).
|
||||
| [Configuration](Configuration/findings.md) | 2026-11-09 | `91fb2b4` | 0/0/0/0 | 0 | 4 |
|
||||
| [GalaxyRepository](GalaxyRepository/findings.md) | 2026-11-09 | `91fb2b4` | 0/0/0/0 | 0 | 8 |
|
||||
| [Health](Health/findings.md) | 2026-11-09 | `91fb2b4` | 0/0/0/0 | 0 | 7 |
|
||||
| [LocalDb](LocalDb/findings.md) | 2026-11-09 | `91fb2b4` | 0/0/4/3 | 7 | 7 |
|
||||
| [LocalDb](LocalDb/findings.md) | 2026-11-09 | `91fb2b4` | 0/0/3/3 | 6 | 7 |
|
||||
| [Overview](Overview/findings.md) | 2026-11-09 | `91fb2b4` | 0/0/0/0 | 0 | 5 |
|
||||
| [Secrets](Secrets/findings.md) | 2026-11-09 | `91fb2b4` | 0/0/0/13 | 13 | 19 |
|
||||
| [Telemetry](Telemetry/findings.md) | 2026-11-09 | `91fb2b4` | 0/0/0/0 | 0 | 11 |
|
||||
@@ -84,11 +84,10 @@ _None open._
|
||||
|
||||
_None open._
|
||||
|
||||
### Medium (4)
|
||||
### Medium (3)
|
||||
|
||||
| ID | Library | Title |
|
||||
|----|---------|-------|
|
||||
| LocalDb-001 | [LocalDb](LocalDb/findings.md) | Sync endpoint is unauthenticated by default, and `ApiKey` is documented as "expected from the peer" |
|
||||
| LocalDb-002 | [LocalDb](LocalDb/findings.md) | Snapshot streams tables by name and rows by `pk_json` text, so FK children land before parents and are dead-lettered for good |
|
||||
| LocalDb-003 | [LocalDb](LocalDb/findings.md) | `INSERT OR REPLACE` apply silently deletes a different row on a secondary `UNIQUE` conflict, so the pair diverges |
|
||||
| LocalDb-004 | [LocalDb](LocalDb/findings.md) | `FailClosedOnDrift` is bypassed on the snapshot path, so a future-stamped snapshot row moves the local HLC forward permanently |
|
||||
|
||||
Reference in New Issue
Block a user