← Back to PRs

#17746: fix(gateway): add shared-secret fallback to trusted-proxy auth dispatcher

by dashed open 2026-02-16 04:36 View on GitHub →
gateway size: XL
## Summary Fixes #17761. **Stacked on #17705** — review/merge that PR first. Once merged, this PR's diff will automatically update to show only the changes from this PR. The gateway's `authorizeGatewayConnect` dispatcher treats `trusted-proxy` as a single-mode gate: when proxy auth fails (e.g. internal services connecting directly without the reverse proxy), the function early-returns before reaching the shared-secret (token/password) or Tailscale code paths. This breaks all internal consumers — node host, CLI RPC, ACP, TUI, agent tools, etc. This PR adds an inline shared-secret fallback within the trusted-proxy block: - When proxy auth fails and a token/password is configured, attempt shared-secret auth before returning failure - Rate-limit fallback attempts using the existing `AuthRateLimiter` - Preserve proxy auth priority (successful proxy auth still short-circuits) - Fix `allowTailscale` default to not exclude `trusted-proxy` mode ## Root Cause When `auth.mode === "trusted-proxy"`, the `authorizeGatewayConnect` function enters the trusted-proxy block and either succeeds or returns a failure reason. It never falls through to the shared-secret (token/password) block below. Internal services that connect directly (without the reverse proxy) always fail because they can't provide the proxy headers. ## Changes **`src/gateway/auth.ts`**: 1. Hoist `limiter`/`ip`/`rateLimitScope` above the trusted-proxy block so the fallback can reuse them: ```diff + const limiter = params.rateLimiter; + const ip = + params.clientIp ?? + resolveRequestClientIp(req, trustedProxies, params.allowRealIpFallback === true) ?? + req?.socket?.remoteAddress; + const rateLimitScope = params.rateLimitScope ?? AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET; + if (auth.mode === "trusted-proxy") { ``` 2. Add shared-secret fallback after proxy auth failure: ```diff if ("user" in result) { return { ok: true, method: "trusted-proxy", user: result.user }; } + + // Trusted-proxy auth failed — try shared-secret fallback for internal + // services (CLI, node host, ACP) that bypass the reverse proxy. + if (!auth.token && !auth.password) { + return { ok: false, reason: result.reason }; + } + + // Rate-limit fallback attempts + if (limiter) { ... } + + // Try token fallback + if (connectAuth?.token && auth.token) { + if (safeEqualSecret(connectAuth.token, auth.token)) { + limiter?.reset(ip, rateLimitScope); + return { ok: true, method: "token" }; + } + ... + } + + // Try password fallback + if (connectAuth?.password && auth.password) { ... } + + // Client didn't provide matching credentials — return original proxy failure return { ok: false, reason: result.reason }; ``` 3. Fix `allowTailscale` default to not exclude `trusted-proxy` mode: ```diff - authConfig.allowTailscale ?? - (params.tailscaleMode === "serve" && mode !== "password" && mode !== "trusted-proxy"); + authConfig.allowTailscale ?? (params.tailscaleMode === "serve" && mode !== "password"); ``` **`src/gateway/auth.test.ts`**: 9 new unit tests for the fallback path (success, rejection, rate limiting, priority) **`src/gateway/server.auth.e2e.test.ts`**: 3 new e2e tests for internal connections with token fallback + device identity ### Connection flow (after fix) ``` Client connects to gateway (mode=trusted-proxy) ├─ From trusted proxy with user header? → Authenticated (trusted-proxy) ├─ Proxy auth failed, token/password configured? │ ├─ Rate-limited? → Rejected (rate_limited) │ ├─ Valid token? → Authenticated (token) → device-pairing works │ ├─ Valid password? → Authenticated (password) → device-pairing works │ └─ No match → Original proxy failure reason └─ No fallback credentials configured → Original proxy failure reason ``` ## Related Issues - #17761 — this bug report - #1560 — original trusted-proxy auth implementation - #17270 — device_token_mismatch errors in trusted-proxy setups - #17106 — `canSkipDevice`/`skipPairing` gate logic - #10382 — auth mode configuration - #4833 — GatewayClient reconnect behavior - #5559 — rate limiting for auth ## Test Plan - [x] 9 unit tests for shared-secret fallback (proxy success unchanged, token/password fallback, rejection, rate limiting, priority) - [x] 3 e2e tests for internal connections (token + device identity, token + no device, proxy priority) - [x] All existing 30 unit tests pass - [x] All existing 33 e2e tests pass - [x] `oxlint` — 0 errors - [x] `oxfmt` — clean - [x] `tsgo` — clean Closes #17761 Related: #8529, #7384, #4833

Most Similar PRs