Skip to content

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

Closed
dashed wants to merge 3 commits intoopenclaw:mainfrom
dashed:fix/trusted-proxy-auth-fallback
Closed

fix(gateway): add shared-secret fallback to trusted-proxy auth dispatcher#17746
dashed wants to merge 3 commits intoopenclaw:mainfrom
dashed:fix/trusted-proxy-auth-fallback

Conversation

@dashed
Copy link
Copy Markdown
Contributor

@dashed dashed commented Feb 16, 2026

Summary

Fixes #17761.

Note: The device-pairing bypass for trusted-proxy connections (previously PR #17705) has been absorbed upstream via the connect-policy.ts refactoring (isTrustedProxyControlUiOperatorAuth, evaluateMissingDeviceIdentity, shouldSkipControlUiPairing). This PR is now based directly on main and focuses solely on the shared-secret auth fallback.

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:
+  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") {
  1. Add shared-secret fallback after proxy auth failure:
     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 };
  1. Fix allowTailscale default to not exclude trusted-proxy mode:
-    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

Test Plan

  • 9 unit tests for shared-secret fallback (proxy success unchanged, token/password fallback, rejection, rate limiting, priority)
  • 3 e2e tests for internal connections (token + device identity, token + no device, proxy priority)
  • All existing 30 unit tests pass
  • All existing 33 e2e tests pass
  • oxlint — 0 errors
  • oxfmt — clean
  • tsgo — clean

Closes #17761
Related: #8529, #7384, #4833


Rebase History

Date Base Upstream Commits Notes
2026-03-23 d5917d37c54a (post-v2026.3.23) 929 Rebased cleanly. Tip commit (typing fix) now empty — upstream absorbed that change.
2026-03-13 330631a0eb39 (v2026.3.12) - Clean rebase. Upstream extracted resolveRequestClientIp to net.ts and added bootstrap tokens. No conflicts. Commits: 55d625971175, ddd1db37ffa0, f0cf25e4a891.
2026-03-08 eb0758e1722c (v2026.3.7) - Clean rebase, no conflicts. Upstream hardened gateway auth resolution across systemd/discord/node host, unified SecretRef credential readers, and added auth rate limiting — all complementary to this PR's shared-secret fallback. Commits: fbec3dcadecb, f8b31778c1ad, c448c932f6d5.
2026-02-27 Previous base - Rebased directly onto main after fix/trusted-proxy-device-pairing was absorbed upstream via connect-policy.ts refactoring.

@dashed
Copy link
Copy Markdown
Contributor Author

dashed commented Feb 22, 2026

Note on Windows CI failure: The checks-windows (node, test) failure is a pre-existing issue on main — not caused by this PR.

The test `passes manager-scoped XDG env to mcporter commands` in `src/memory/qmd-manager.test.ts` (introduced by #19617 / commit 3317b49) uses hardcoded Unix forward slashes in path assertions that fail on Windows. Main's own CI run 22268115588 shows the same failure.

Fix PR: #23128

@dashed dashed force-pushed the fix/trusted-proxy-auth-fallback branch 2 times, most recently from 4a9e169 to 4647d8a Compare February 27, 2026 07:29
@dashed dashed force-pushed the fix/trusted-proxy-auth-fallback branch from 4647d8a to 5b5b4ae Compare February 27, 2026 11:56
@dashed dashed force-pushed the fix/trusted-proxy-auth-fallback branch 3 times, most recently from 9af0b9d to c448c93 Compare March 8, 2026 13:39
@lzu-zhanghr
Copy link
Copy Markdown

hello, @dashed Would you mind following up on this PR to get it landed?

@dashed dashed force-pushed the fix/trusted-proxy-auth-fallback branch 2 times, most recently from f0cf25e to 2503730 Compare March 13, 2026 22:43
@openclaw-barnacle
Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Mar 20, 2026
@dashed dashed force-pushed the fix/trusted-proxy-auth-fallback branch from 2503730 to b72d997 Compare March 21, 2026 05:36
@dashed dashed requested a review from a team as a code owner March 21, 2026 05:36
@dashed dashed force-pushed the fix/trusted-proxy-auth-fallback branch from b72d997 to 2a73484 Compare March 21, 2026 05:49
@openclaw-barnacle openclaw-barnacle Bot removed the stale Marked as stale due to inactivity label Mar 22, 2026
dashed and others added 3 commits March 23, 2026 22:40
…cher

When auth.mode is "trusted-proxy" and proxy auth fails (e.g. internal
connections that bypass the reverse proxy), fall back to token/password
credentials if configured. This allows CLI, node hosts, ACP, and other
internal services to authenticate directly while external users
authenticate via the proxy.

Also enable the tailscale overlay for trusted-proxy mode by removing the
mode exclusion from the allowTailscale default.
Add 9 unit tests covering fallback behavior: proxy success unchanged,
token/password fallback on valid credentials, rejection on mismatch,
no-fallback when server credentials unconfigured, rate limiting on
fallback attempts, and proxy-takes-priority when both are available.

Add 3 e2e tests covering internal connection scenarios: token auth with
device identity, token auth without device identity (canSkipDevice),
and proxy connection priority over token fallback.
@dashed dashed force-pushed the fix/trusted-proxy-auth-fallback branch from 2a73484 to 42556d7 Compare March 24, 2026 02:47
@vincentkoc
Copy link
Copy Markdown
Member

Closing this in favor of #54536.

The root cause and touched surface are the same: trusted-proxy local/internal callers need a shared-secret fallback instead of hard failure when proxy identity headers are absent.

#54536 is now the active maintainer path because it carries the safer follow-up hardening and test coverage that landed during review:

  • local fallback requires token auth instead of bypassing auth
  • loopback requests do not get trusted-proxy identity-header auth
  • runtime validation is aligned with the new loopback model

Credit preserved here: this PR framed the shared-secret fallback bug clearly and drove the canonical bug path for #17761.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gateway Gateway runtime size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Gateway auth dispatcher blocks all internal services when mode=trusted-proxy (no shared-secret fallback)

4 participants