Skip to content

fix(gateway): disconnect WS sessions authed against a rotated shared secret (#2722)#2731

Merged
alexey-pelykh merged 1 commit into
mainfrom
fix/gateway-rotate-shared-secret-disconnect-ws-2722
Jun 18, 2026
Merged

fix(gateway): disconnect WS sessions authed against a rotated shared secret (#2722)#2731
alexey-pelykh merged 1 commit into
mainfrom
fix/gateway-rotate-shared-secret-disconnect-ws-2722

Conversation

@alexey-pelykh

Copy link
Copy Markdown

Summary

Closes #2722. Rotating the shared gateway auth secret via config.set / config.patch / config.apply left WebSocket sessions that authenticated against the old secret connected until the next gateway restart — credential revocation was ineffective for live connections (HIGH).

This wires the shared-core disconnect driver together with its config-write caller. They must land together: the unit-level caller is inert without the integration path in server.impl.ts, which is why the two covering tests were it.skip-ped as one deferred hardening item.

Changes

  • src/gateway/server.impl.tsgatewayRequestContext.disconnectClientsUsingSharedGatewayAuth() iterates the connected-client set and closes every usesSharedGatewayAuth socket with code 4001 "gateway auth changed".
  • src/gateway/server-methods/types.ts — declares the optional disconnectClientsUsingSharedGatewayAuth?: () => void on GatewayRequestContext.
  • src/gateway/server-methods/config.ts — detects an effective shared-auth change (active mode's secret only, via the existing resolveEffectiveSharedGatewayAuth) and queues the disconnect after the success response:
    • config.set — when reload is off (no watcher restart would otherwise drop the stale sessions).
    • config.patch / config.apply — unconditionally on an effective rotation (revoke immediately rather than wait out the delayed SIGUSR1 restart).
  • Un-skipped the two acceptance-gate tests: server.shared-token-session-rotation.test.ts and server-methods/config.shared-auth.test.ts.

The connection-side flag (GatewayWsClient.usesSharedGatewayAuth, set in message-handler.ts) was already present in the fork; only the driver, the context-type field, and the caller wiring were missing.

Device-token and other non-shared sessions are preserved by the usesSharedGatewayAuth guard.

Acceptance criteria

  • Both previously-skipped tests pass.
  • After a shared-secret rotation, WS sessions authed against the prior secret are disconnected without requiring a restart. (The integration test starts a real server with reload.mode: "off" — no restart path — and asserts the old-token socket closes 4001.)

Testing

  • pnpm test:gateway (full suite): 141 files, 1559 passed, 7 skipped (the 7 are unrelated pre-existing deferrals). No regressions.
  • pnpm check: format ✓, typecheck ✓, lint 0/0 ✓, css-class-drift 0 orphans ✓.
  • Fork-integrity gates: throwing-stub-callers, stub-debt, zombie-imports all green.
  • A fresh-context adversarial security review (security-architect) probed five residual revocation surfaces and found no blocking holes; it independently confirmed that the new-connection path is closed by the existing config-cache flush in the IO write path.

Follow-up (out of scope — not bundled)

The security review noted an orphaned dead-code module src/gateway/server-shared-auth-generation.ts (plus the unused sharedGatewaySessionGeneration? field in ws-types.ts) — an upstream-sync artifact implementing a parallel generation-based revocation mechanism with zero live callers. It is inert (not a credential hole) but worth a separate gut/cleanup pass to avoid future confusion. Intentionally not addressed here to keep this fix scoped.

🤖 Generated with Claude Code

…secret (#2722)

Rotating the shared gateway auth secret via config.set/patch/apply left
WebSocket sessions that authenticated with the OLD secret connected until the
next restart — credential revocation was ineffective for live connections.

Wire the shared-core disconnect driver together with its config-write caller
(the unit-level caller is inert without the integration path in server.impl.ts,
so they must land together):

- server.impl.ts: gatewayRequestContext.disconnectClientsUsingSharedGatewayAuth
  closes every usesSharedGatewayAuth socket with code 4001 "gateway auth changed".
- server-methods/types.ts: declare the optional context method.
- server-methods/config.ts: detect an effective shared-auth change (active mode's
  secret only, via the existing resolveEffectiveSharedGatewayAuth) and queue the
  disconnect — config.set when reload is off (no watcher restart would drop them),
  config.patch/config.apply unconditionally (revoke immediately rather than wait
  out the delayed SIGUSR1 restart).
- Un-skip the two covering tests (server.shared-token-session-rotation,
  config.shared-auth) that are the acceptance gate.

Device-token and other non-shared sessions are preserved by the
usesSharedGatewayAuth guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@alexey-pelykh
alexey-pelykh merged commit f762046 into main Jun 18, 2026
17 checks passed
@alexey-pelykh
alexey-pelykh deleted the fix/gateway-rotate-shared-secret-disconnect-ws-2722 branch June 18, 2026 09:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gateway: rotating the shared secret does not disconnect WS sessions authed against the old secret

1 participant