#17746: fix(gateway): add shared-secret fallback to trusted-proxy auth dispatcher
gateway
size: XL
Cluster:
Device Auth and Security Fixes
## 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
#17705: fix(gateway): allow trusted-proxy auth to bypass device-pairing gates
by dashed · 2026-02-16
83.0%
#19937: fix(gateway): validate token/password auth modes and isolate gatewa...
by NewdlDewdl · 2026-02-18
78.0%
#23355: Gateway: fail closed on untrusted proxy headers
by bmendonca3 · 2026-02-22
77.2%
#23425: Gateway: require trusted-proxy allowlist unless allowAll is explicit
by bmendonca3 · 2026-02-22
76.4%
#17378: fix(gateway): allow dangerouslyDisableDeviceAuth with trusted-proxy...
by ar-nadeem · 2026-02-15
75.9%
#19389: Fix #2248: Allow insecure auth bypass when device signature validat...
by cedillarack · 2026-02-17
74.9%
#19885: test(gateway,browser): isolate tests from ambient OPENCLAW_GATEWAY_...
by NewdlDewdl · 2026-02-18
74.9%
#21651: fix(gateway): token fallback + operator.admin scope superset in pai...
by lan17 · 2026-02-20
73.0%
#22766: fix(security): enable gateway auth rate limiting by default (CWE-307)
by brandonwise · 2026-02-21
72.8%
#8513: Gateway: require auth for plugin HTTP
by coygeek · 2026-02-04
72.7%