Skip to content

auth: allow local loopback connections to bypass trusted-proxy auth#51070

Closed
dragonforce2010 wants to merge 1 commit intoopenclaw:mainfrom
dragonforce2010:feat/trusted-proxy-local-bypass
Closed

auth: allow local loopback connections to bypass trusted-proxy auth#51070
dragonforce2010 wants to merge 1 commit intoopenclaw:mainfrom
dragonforce2010:feat/trusted-proxy-local-bypass

Conversation

@dragonforce2010
Copy link
Copy Markdown

No description provided.

@dragonforce2010 dragonforce2010 requested a review from a team as a code owner March 20, 2026 13:58
@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime size: S labels Mar 20, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 20, 2026

Greptile Summary

This PR extends the existing isLocalDirectRequest loopback-bypass pattern — already applied to canvas requests, session-kill, and WebSocket message handling — to trusted-proxy auth mode. When a connection arrives directly on the loopback interface (no forwarded headers, localish Host), it is granted access with method: "none" before any proxy-header checks, allowing local CLI commands to reach the gateway without proxy identity headers.

Key observations:

  • The bypass is placed correctly before the auth.trustedProxy config and trustedProxies list checks, so it also covers misconfigured trusted-proxy setups for local connections.
  • The returned { ok: true, method: "none" } is consistent with every other local-direct bypass in the codebase (canvas, session-kill, etc.).
  • One subtle edge case: if trustedProxies contains 127.0.0.1 (e.g. a local nginx reverse proxy on the same machine), resolveRequestClientIp resolves to undefined for header-less loopback requests (the proxy IP is trusted but no forwarded chain exists), causing isLocalDirectRequest to return false. The bypass silently doesn't apply and the CLI would fail with trusted_proxy_user_missing. This is a corner case but worth being aware of.
  • Test coverage covers the primary IPv4 loopback path and the non-loopback rejection; an IPv6 loopback test (::1) is missing.

Confidence Score: 4/5

  • Safe to merge — the change is small, follows an established pattern in the codebase, and cannot be triggered by external connections (TCP remoteAddress must genuinely be loopback).
  • The logic is simple and consistent with how isLocalDirectRequest is used elsewhere. The only notable gap is the absence of an IPv6 loopback test, which means confidence in ::1 behavior relies on the correctness of isLoopbackIpAddress rather than an explicit end-to-end test for this bypass path. No functional bugs or security issues were found.
  • No files require special attention; the one suggestion is a minor test-coverage improvement in src/gateway/auth.test.ts.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/gateway/auth.test.ts
Line: 653-693

Comment:
**Missing IPv6 loopback test case**

The two new tests only exercise `remoteAddress: "127.0.0.1"`. The gateway also binds to `::1` when available (see `resolveGatewayListenHosts` in `net.ts`), so a CLI that connects via `[::1]` should receive the same bypass. Adding a third test with `remoteAddress: "::1"` and `host: "[::1]:18789"` would confirm the bypass works symmetrically on IPv6 loopback.

```ts
it("allows local direct IPv6 loopback connections to bypass trusted-proxy auth", async () => {
  const res = await authorizeGatewayConnect({
    auth: {
      mode: "trusted-proxy",
      allowTailscale: false,
      trustedProxy: trustedProxyConfig,
    },
    connectAuth: null,
    trustedProxies: ["10.0.0.1"],
    req: {
      socket: { remoteAddress: "::1" },
      headers: {
        host: "[::1]:18789",
      },
    } as never,
  });

  expect(res.ok).toBe(true);
  expect(res.method).toBe("none");
});
```

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: "auth: allow local lo..."

Comment thread src/gateway/auth.test.ts
Comment on lines +653 to +693
it("allows local direct loopback connections to bypass trusted-proxy auth", async () => {
const res = await authorizeGatewayConnect({
auth: {
mode: "trusted-proxy",
allowTailscale: false,
trustedProxy: trustedProxyConfig,
},
connectAuth: null,
trustedProxies: ["10.0.0.1"],
req: {
socket: { remoteAddress: "127.0.0.1" },
headers: {
host: "127.0.0.1:18789",
},
} as never,
});

expect(res.ok).toBe(true);
expect(res.method).toBe("none");
});

it("does not bypass trusted-proxy auth for non-loopback connections", async () => {
const res = await authorizeGatewayConnect({
auth: {
mode: "trusted-proxy",
allowTailscale: false,
trustedProxy: trustedProxyConfig,
},
connectAuth: null,
trustedProxies: ["10.0.0.1"],
req: {
socket: { remoteAddress: "192.168.1.50" },
headers: {
host: "192.168.1.50:18789",
},
} as never,
});

expect(res.ok).toBe(false);
expect(res.reason).toBe("trusted_proxy_untrusted_source");
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing IPv6 loopback test case

The two new tests only exercise remoteAddress: "127.0.0.1". The gateway also binds to ::1 when available (see resolveGatewayListenHosts in net.ts), so a CLI that connects via [::1] should receive the same bypass. Adding a third test with remoteAddress: "::1" and host: "[::1]:18789" would confirm the bypass works symmetrically on IPv6 loopback.

it("allows local direct IPv6 loopback connections to bypass trusted-proxy auth", async () => {
  const res = await authorizeGatewayConnect({
    auth: {
      mode: "trusted-proxy",
      allowTailscale: false,
      trustedProxy: trustedProxyConfig,
    },
    connectAuth: null,
    trustedProxies: ["10.0.0.1"],
    req: {
      socket: { remoteAddress: "::1" },
      headers: {
        host: "[::1]:18789",
      },
    } as never,
  });

  expect(res.ok).toBe(true);
  expect(res.method).toBe("none");
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/auth.test.ts
Line: 653-693

Comment:
**Missing IPv6 loopback test case**

The two new tests only exercise `remoteAddress: "127.0.0.1"`. The gateway also binds to `::1` when available (see `resolveGatewayListenHosts` in `net.ts`), so a CLI that connects via `[::1]` should receive the same bypass. Adding a third test with `remoteAddress: "::1"` and `host: "[::1]:18789"` would confirm the bypass works symmetrically on IPv6 loopback.

```ts
it("allows local direct IPv6 loopback connections to bypass trusted-proxy auth", async () => {
  const res = await authorizeGatewayConnect({
    auth: {
      mode: "trusted-proxy",
      allowTailscale: false,
      trustedProxy: trustedProxyConfig,
    },
    connectAuth: null,
    trustedProxies: ["10.0.0.1"],
    req: {
      socket: { remoteAddress: "::1" },
      headers: {
        host: "[::1]:18789",
      },
    } as never,
  });

  expect(res.ok).toBe(true);
  expect(res.method).toBe("none");
});
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c14cd23c50

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/gateway/auth.ts
Comment on lines +388 to +389
if (localDirect) {
return { ok: true, method: "none" };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Don't bypass trusted-proxy auth for all loopback clients

This turns every direct 127.0.0.1/::1 request into { ok: true, method: "none" } whenever gateway.auth.mode="trusted-proxy", which means any unprivileged local user/process on the host can skip the proxy's identity checks and hit operator-only surfaces like src/gateway/tools-invoke-http.ts:159 and src/gateway/sessions-history-http.ts:158 with no headers or shared secret. That is a real auth regression for same-host proxy deployments, and it also contradicts the repo's documented guarantee that loopback auth exists to block other local processes (docs/help/faq.md:1350).

Useful? React with 👍 / 👎.

@nickytonline
Copy link
Copy Markdown
Contributor

This bypasses trusted-proxy auth entirely for loopback connections, not just calls via the CLI, returning { ok: true, method: "none" }. I'm more inclined to not bypass auth here at all.

The trusted-proxy mode exists so the reverse proxy handles identity for every connection. This creates a path with no identity — no user, no audit trail.

A few concerns:

  • If anything on the same box has a Server-Side Request Forgery (SSRF) vulnerability (where an attacker tricks a server into making requests on their behalf), that's an unauthed path straight to the gateway via 127.0.0.1.
  • In Docker, the loopback isolation assumption gets fragile fast — shared network namespaces, port mappings, k8s pods.
  • method: "none" means these connections have no user attribution. That's going to make debugging in prod harder than it needs to be.

I get that local CLI commands don't go through the proxy and lack identity headers. A local service account token would solve that without opening up a full auth bypass.

Here's how it maps out:

Current state (no PR):

Request source What happens
Remote via proxy Proxy headers validated from trusted IP — user identity established
Local CLI on loopback Rejected — no proxy headers, no trusted IP

With this PR:

Request source What happens
Remote via proxy Same — works fine
Local CLI on loopback Bypassed entirely — method: "none", no identity
SSRF on loopback Also bypassed — same free pass

With a local service account token instead:

Request source What happens
Remote via proxy Same — works fine
Local CLI on loopback + valid token Authenticated as e.g. local-cli@localhost — auditable
Local CLI on loopback + no token Rejected
SSRF on loopback Rejected — attacker doesn't have the token

The token would work similarly to token auth mode but scoped to loopback only — auto-generated, file-permission-restricted (0600), and only valid on local connections. Even if it leaks, it's not usable remotely.

@nickytonline
Copy link
Copy Markdown
Contributor

Also can you add a description to the PR @dragonforce2010 ?

@nickytonline
Copy link
Copy Markdown
Contributor

Just came across #17746, and this is similar to what I'm proposing.

@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 26, 2026

Closing this as duplicate or superseded after Codex automated review.

Close PR #51070 as superseded by the open service-identity/trusted-proxy RFC #69066. Current main intentionally keeps trusted-proxy loopback requests fail-closed, with tests and docs enforcing that contract, and the PR's blanket method: "none" loopback bypass conflicts with the review discussion's service-account-token direction.

Best possible solution:

Do not merge the blanket loopback method: "none" bypass. Keep the current fail-closed trusted-proxy loopback contract on main, and route the real local/internal-auth problem through the open service-identity RFC #69066 plus any narrower opt-in trusted-proxy fixes that maintain identity, rate limiting, and auditability.

What I checked:

So I’m closing this here and keeping the remaining discussion on the canonical linked item.

Codex Review notes: model gpt-5.5, reasoning high; reviewed against 4ad8b613c9b6.

@clawsweeper clawsweeper Bot closed this Apr 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gateway Gateway runtime size: S triage: blank-template Candidate: PR template appears mostly untouched.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants