fix(SEC-33,SEC-34): host-meaningful path rooting; verification-cache invalidate race

SEC-33: make rooting host-meaningful and stop shipping foreign-platform literals.
- Delete IsRootedForAnyPlatform; AddIfNotRooted now uses Path.IsPathRooted (current OS).
- Promote AddIfNotRooted/AddIfInvalidPath to shared GatewayConfigPathRules so the new
  Galaxy validator reuses them and the two validators cannot drift.
- Remove Authentication:SqlitePath and Galaxy:SnapshotCachePath Windows literals from
  appsettings.json; the CommonApplicationData-derived code defaults take over. The
  Galaxy default is seeded as a configuration value before AddZbGalaxyRepository
  (SnapshotCachePath is init-only, so a PostConfigure mutation cannot compile).
- New GalaxyRepositoryOptionsValidator (ValidateOnStart) enforces a valid, host-rooted
  SnapshotCachePath when PersistSnapshot is true.
- Root-cause the stray junk-named auth DB: host start eagerly builds
  AuthSqliteConnectionFactory; under the non-rooted Windows literal on macOS SQLite
  wrote it relative to the test bin CWD. The three real-host-start tests now pin
  SqlitePath to a temp path.

SEC-34: verification cache Invalidate-vs-in-flight-repopulation race closed with a
per-key generation counter (bump-before-evict, snapshot-then-recheck). The expiry
cap (window 2) takes the documented fallback: the library verification identity
carries no ExpiresUtc, so the cache cannot cap at the key's expiry (donor-library ask).

GWC-24 rider: cap MxGateway:Events:QueueCapacity at int.MaxValue/2 so the derived
checked(2 * EventChannelCapacity) in WorkerClient cannot overflow at session creation.

SEC-35 (doc-only): note IsProduction() env-name semantics in GatewayConfiguration.md.

Docs updated same commit (GatewayConfiguration.md, Authentication.md) and tracking
registers/change-log flipped (00-tracking.md, 40-security-dashboard.md).
This commit is contained in:
Joseph Doherty
2026-08-07 06:36:01 -04:00
parent 3f854d6cbf
commit 7e7f7cad84
15 changed files with 548 additions and 112 deletions
+22 -7
View File
@@ -99,9 +99,20 @@ library:
are skipped. Only successes are cached; failures always reach the inner verifier.
On a gateway-initiated revoke/rotate/delete the dashboard admin service calls
`IApiKeyCacheInvalidator.Invalidate(keyId)`, evicting the cached entry
immediately. The short TTL is the backstop for out-of-band mutations (a direct DB
edit, or a revoke run by the separate `apikey` CLI process, whose in-memory cache
is not the running gateway's cache).
immediately. `Invalidate` bumps a per-key generation counter **before** it evicts,
and `VerifyAsync` snapshots that generation before the inner verify and re-checks
it after writing the cache entry (set-then-recheck); a revoke that lands while a
verification is still in flight in the inner library therefore discards that
verification's repopulation instead of re-caching the just-revoked identity for a
full TTL (SEC-34). The short TTL remains the backstop for two bounded-staleness
windows it cannot close directly: (1) out-of-band mutations (a direct DB edit, or a
revoke run by the separate `apikey` CLI process, whose in-memory cache is not the
running gateway's cache); and (2) a key whose `ExpiresUtc` passes while cached keeps
authenticating until the entry's TTL elapses — expiry is enforced by the inner
library verifier, which a cache hit never reaches, and the verification identity the
library returns carries no expiry timestamp, so the cache cannot cap an entry at the
key's expiry (capping it needs the donor library to surface expiry on the
verification identity). The default 15 s TTL bounds both windows.
- **`CoalescingMarkApiKeyStore`** wraps the library `IApiKeyStore` and forwards at
most one `MarkUsed` write per key per
`MxGateway:Security:ApiKeyLastUsedCoalesceSeconds` (default 60 s), so even under a
@@ -148,10 +159,14 @@ is derived from `Environment.GetFolderPath(SpecialFolder.CommonApplicationData)`
(`C:\ProgramData\MxGateway\gateway-auth.db` on Windows,
`/usr/share/MxGateway/gateway-auth.db` or the container equivalent elsewhere) so the
credential store is never written relative to the launch working directory on a
non-Windows host. The production hosts pin the explicit Windows path in
`appsettings.json`. `GatewayOptionsValidator` rejects a non-rooted (relative)
`SqlitePath` so a bad override fails fast at startup rather than scattering the store
by launch CWD (SEC-01).
non-Windows host. `appsettings.json` no longer ships an explicit path (SEC-33): the
removed Windows literal matched the Windows code default and, being non-rooted on a
Unix host, would have resolved against the CWD there; deployed hosts override it
through the NSSM environment (`MxGateway__Authentication__SqlitePath`).
`GatewayOptionsValidator` rejects a `SqlitePath` that is not rooted **on the host
running the gateway** (`Path.IsPathRooted`, current OS) — a relative filename or a
foreign-platform literal fails fast at startup rather than scattering the store by
launch CWD (SEC-01, SEC-33).
The library owns the SQLite schema and connection factory. The `api_keys` table
carries the key id, key prefix, secret-hash blob, display name, serialized scopes,