Skip to content

fix(auth): enhance trusted-proxy handling for non-loopback forwarded IPs#59190

Closed
Sandblaze05 wants to merge 13 commits intoopenclaw:mainfrom
Sandblaze05:fix-#59167
Closed

fix(auth): enhance trusted-proxy handling for non-loopback forwarded IPs#59190
Sandblaze05 wants to merge 13 commits intoopenclaw:mainfrom
Sandblaze05:fix-#59167

Conversation

@Sandblaze05
Copy link
Copy Markdown

Summary

  • Problem: Trusted-proxy auth rejected all loopback sources, which broke same-host reverse proxy setups (for example Caddy or nginx on localhost) even when gateway.trustedProxies explicitly included loopback.
  • Why it matters: Local authn/authz proxy deployments could no longer access Gateway, causing unauthorized failures with reason trusted_proxy_loopback_source.
  • What changed: In trusted-proxy mode, loopback proxy sources are now allowed only when forwarded client IP resolves to a non-loopback address.
  • What did NOT change (scope boundary): Direct loopback requests without valid forwarded client origin still fail closed; no token/password auth behavior changed.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Root Cause / Regression History (if applicable)

  • Root cause: Trusted-proxy authorization added a blanket loopback-source rejection in src/gateway/auth.ts, so trusted identity headers from localhost proxies were always denied.
  • Missing detection / guardrail: No positive test existed for same-host trusted proxy with non-loopback forwarded client origin.
  • Prior context (git blame, prior PR, issue, or refactor if known): Regression introduced in the hardening change from fix(gateway/auth): local trusted-proxy fallback to require token auth #54536.
  • Why this regressed now: The loopback block was unconditional, so explicit trustedProxies loopback configurations could not succeed.
  • If unknown, what was ruled out: N/A

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
  • Unit test
  • Seam / integration test
  • End-to-end test
  • Existing coverage already sufficient
  • Target test or file: auth.test.ts
  • Scenario the test should lock in: Accept same-host trusted-proxy auth when x-forwarded-for resolves to a non-loopback client IP, while still rejecting missing/loopback client chains.
  • Why this is the smallest reliable guardrail: Logic is isolated in gateway auth path and can be validated deterministically in unit tests.
  • Existing test that already covers this (if any): Existing tests already covered loopback rejection and fail-closed behavior.
  • If no new test is added, why not: N/A (new test added)

User-visible / Behavior Changes

  • Trusted-proxy mode on loopback now works for local reverse proxies if forwarded client origin is present and non-loopback.
  • Requests still fail with trusted_proxy_loopback_source when forwarded client origin is missing or resolves to loopback.

Diagram (if applicable)

Before:
[localhost proxy request with identity headers] -> [loopback source check] -> [always reject]

After:
[localhost proxy request with identity headers]
-> [loopback source check]
-> [resolve forwarded client IP]
-> [non-loopback client IP: allow trusted-proxy auth]
-> [missing/loopback client IP: reject]

Security Impact (required)

  • New permissions/capabilities? (No)
  • Secrets/tokens handling changed? (No)
  • New/changed network calls? (No)
  • Command/tool execution surface changed? (No)
  • Data access scope changed? (No)
  • If any Yes, explain risk + mitigation: N/A

Repro + Verification

Environment

  • OS: Windows 11 dev environment; issue report target is Debian 13
  • Runtime/container: local test runner
  • Model/provider: N/A
  • Integration/channel (if any): Gateway trusted-proxy auth
  • Relevant config (redacted): trusted-proxy mode with trustedProxies including 127.0.0.1

Steps

  1. Configure gateway auth mode trusted-proxy with trustedProxies including loopback.
  2. Send request from loopback source with trusted identity headers and x-forwarded-for as non-loopback client IP.
  3. Validate auth succeeds; verify loopback-only or missing forwarded client origin still fails.

Expected

  • Same-host proxy auth succeeds when forwarded client origin is non-loopback.

Actual

  • Previously all loopback trusted-proxy requests were rejected; now behavior matches expected with fail-closed safety preserved.

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Scoped verification run:

  • corepack pnpm test -- auth.test.ts -t "accepts same-host proxy identity headers when forwarded client IP is non-loopback|rejects trusted-proxy identity headers from loopback sources|fails closed when forwarded headers are present but the client chain resolves to loopback"
  • Result: 3 passed, 0 failed

Human Verification (required)

  • Verified scenarios: New same-host trusted-proxy success path with non-loopback x-forwarded-for; existing loopback fail-closed paths.
  • Edge cases checked: Missing forwarded headers and loopback-only forwarded chain still rejected.
  • What you did not verify: Full external reverse-proxy end-to-end (Caddy/Authelia) in this branch.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? (Yes)
  • Config/env changes? (No)
  • Migration needed? (No)
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

  • Risk: Proxies that do not send client-origin forwarding headers remain blocked on loopback.
  • Mitigation: This is intentional fail-closed behavior; deployment guidance should require trusted proxy to set x-forwarded-for with real client IP.

#59167

Copilot AI review requested due to automatic review settings April 1, 2026 18:08
@Sandblaze05 Sandblaze05 requested a review from a team as a code owner April 1, 2026 18:08
@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime size: XS labels Apr 1, 2026
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: c0c1934bcc

ℹ️ 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 Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adjusts Gateway trusted-proxy authentication to support same-host reverse proxies on loopback only when a non-loopback client origin is provided via forwarded IP headers, restoring intended localhost proxy deployments broken by prior loopback-source rejection hardening.

Changes:

  • Allow loopback proxy sources in trusted-proxy mode when x-forwarded-for resolves to a non-loopback client IP; otherwise continue failing closed.
  • Add a unit test covering the new same-host proxy success path with a non-loopback forwarded client IP.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/gateway/auth.ts Relaxes loopback-source rejection for trusted-proxy auth when a non-loopback forwarded client IP is present.
src/gateway/auth.test.ts Adds regression test ensuring same-host trusted-proxy auth succeeds only with a non-loopback forwarded client IP.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 1, 2026

Greptile Summary

This PR fixes a regression (introduced in #54536) where authorizeTrustedProxy unconditionally rejected requests from loopback remote addresses, breaking same-host reverse-proxy setups (e.g., Nginx or Caddy on 127.0.0.1) even when loopback was explicitly listed in trustedProxies.

What changed:

  • src/gateway/auth.ts: The unconditional isLoopbackAddress → reject is replaced with a conditional check: if the remote address is loopback, the function calls resolveRequestClientIp to inspect the x-forwarded-for chain. Only if that chain is absent or resolves back to a loopback address is the request rejected with trusted_proxy_loopback_source. If the chain resolves to a non-loopback client IP the function continues normally into required-headers and user-header validation.
  • src/gateway/auth.test.ts: A new test confirms the new success path (remoteAddr = 127.0.0.1, x-forwarded-for = 203.0.113.10 → auth succeeds with method: "trusted-proxy"). Existing fail-closed tests remain intact and still pass.

Logic correctness: The fix is sound. The outer isTrustedProxyAddress guard (line 349) ensures the loopback address was deliberately configured in trustedProxies before the loopback branch is reached, so trusting the forwarded headers from it is consistent with the operator's intent. The rest of the validation chain (required headers, userHeader, allowUsers) is unchanged and executes after the guard passes.

One test gap: The existing test "rejects same-host proxy request with missing required header" (line 935) does not include x-forwarded-for, so it still fails at the loopback check rather than the required-headers check. The required-headers enforcement on the newly opened code path (loopback source + non-loopback x-forwarded-for + user header present + required header absent) is not covered by any test.

Confidence Score: 5/5

  • Safe to merge; the logic change is correct, fail-closed behavior is preserved, and the only finding is a minor test-coverage gap.
  • No P0 or P1 issues found. The core logic change is correct by inspection and aligns with the documented intent. Fail-closed paths (missing forwarded IP, loopback-only chain) are still rejected. The single P2 comment concerns a missing test for required-headers enforcement on the new code path — not a runtime defect.
  • src/gateway/auth.test.ts — the "rejects same-host proxy request with missing required header" test should be supplemented with a variant that includes a non-loopback x-forwarded-for to actually exercise required-headers enforcement after the loopback guard passes.

Comments Outside Diff (1)

  1. src/gateway/auth.test.ts, line 935-955 (link)

    P2 Missing test for required-headers enforcement on the new loopback-bypass path

    The test named "rejects same-host proxy request with missing required header" doesn't actually exercise the new code path this PR adds. Because the request omits x-forwarded-for, the rejection happens at the loopback check (trusted_proxy_loopback_source), not at the required-headers check — so the comment // missing x-forwarded-proto (requiredHeader) describes the intent, but not what fires.

    Now that a loopback proxy with a non-loopback x-forwarded-for is allowed through, there is no test confirming that requiredHeaders (x-forwarded-proto in this suite) is still enforced after the loopback guard passes. A minimal addition would be:

    it("rejects same-host proxy request with non-loopback client IP but missing required header", async () => {
      const res = await authorizeGatewayConnect({
        auth: {
          mode: "trusted-proxy",
          allowTailscale: false,
          trustedProxy: trustedProxyConfig,
        },
        connectAuth: null,
        trustedProxies: ["127.0.0.1"],
        req: {
          socket: { remoteAddress: "127.0.0.1" },
          headers: {
            host: "localhost",
            "x-forwarded-user": "nick@example.com",
            "x-forwarded-for": "203.0.113.10",
            // x-forwarded-proto intentionally absent
          },
        } as never,
      });
      expect(res.ok).toBe(false);
      expect(res.reason).toBe("trusted_proxy_missing_header_x-forwarded-proto");
    });

    Without this, the required-headers check is untested on the path that this PR opens up.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/gateway/auth.test.ts
    Line: 935-955
    
    Comment:
    **Missing test for required-headers enforcement on the new loopback-bypass path**
    
    The test named `"rejects same-host proxy request with missing required header"` doesn't actually exercise the new code path this PR adds. Because the request omits `x-forwarded-for`, the rejection happens at the loopback check (`trusted_proxy_loopback_source`), not at the required-headers check — so the comment `// missing x-forwarded-proto (requiredHeader)` describes the intent, but not what fires.
    
    Now that a loopback proxy with a non-loopback `x-forwarded-for` is allowed through, there is no test confirming that `requiredHeaders` (`x-forwarded-proto` in this suite) is still enforced after the loopback guard passes. A minimal addition would be:
    
    ```typescript
    it("rejects same-host proxy request with non-loopback client IP but missing required header", async () => {
      const res = await authorizeGatewayConnect({
        auth: {
          mode: "trusted-proxy",
          allowTailscale: false,
          trustedProxy: trustedProxyConfig,
        },
        connectAuth: null,
        trustedProxies: ["127.0.0.1"],
        req: {
          socket: { remoteAddress: "127.0.0.1" },
          headers: {
            host: "localhost",
            "x-forwarded-user": "nick@example.com",
            "x-forwarded-for": "203.0.113.10",
            // x-forwarded-proto intentionally absent
          },
        } as never,
      });
      expect(res.ok).toBe(false);
      expect(res.reason).toBe("trusted_proxy_missing_header_x-forwarded-proto");
    });
    ```
    
    Without this, the required-headers check is untested on the path that this PR opens up.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/gateway/auth.test.ts
Line: 935-955

Comment:
**Missing test for required-headers enforcement on the new loopback-bypass path**

The test named `"rejects same-host proxy request with missing required header"` doesn't actually exercise the new code path this PR adds. Because the request omits `x-forwarded-for`, the rejection happens at the loopback check (`trusted_proxy_loopback_source`), not at the required-headers check — so the comment `// missing x-forwarded-proto (requiredHeader)` describes the intent, but not what fires.

Now that a loopback proxy with a non-loopback `x-forwarded-for` is allowed through, there is no test confirming that `requiredHeaders` (`x-forwarded-proto` in this suite) is still enforced after the loopback guard passes. A minimal addition would be:

```typescript
it("rejects same-host proxy request with non-loopback client IP but missing required header", async () => {
  const res = await authorizeGatewayConnect({
    auth: {
      mode: "trusted-proxy",
      allowTailscale: false,
      trustedProxy: trustedProxyConfig,
    },
    connectAuth: null,
    trustedProxies: ["127.0.0.1"],
    req: {
      socket: { remoteAddress: "127.0.0.1" },
      headers: {
        host: "localhost",
        "x-forwarded-user": "nick@example.com",
        "x-forwarded-for": "203.0.113.10",
        // x-forwarded-proto intentionally absent
      },
    } as never,
  });
  expect(res.ok).toBe(false);
  expect(res.reason).toBe("trusted_proxy_missing_header_x-forwarded-proto");
});
```

Without this, the required-headers check is untested on the path that this PR opens up.

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

Reviews (1): Last reviewed commit: "fix(auth): enhance trusted-proxy handlin..." | Re-trigger Greptile

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: fe9c6a14d9

ℹ️ 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/config/types.gateway.ts Outdated
Comment thread src/gateway/auth.test.ts
@Sandblaze05
Copy link
Copy Markdown
Author

Added unsafeAllowLoopbackProxies configuration flag to address a loopback spoofing vulnerability in trusted-proxy auth.

The issue: The previous loopback exception allowed any local process to forge x-forwarded-for and identity headers, enabling privilege elevation attacks on same-host reverse proxies (nginx/Caddy on localhost).

The solution:

  • Default behavior remains secure: loopback connections are rejected
  • Same-host proxy deployments can opt-in by setting gateway.auth.trustedProxy.unsafeAllowLoopbackProxies: true
  • Configuration includes prominent security warnings and guidance toward safer alternatives (non-loopback address, token/password auth, Tailscale)

Trade-off: Operators with existing localhost reverse proxy setups can now enable this flag consciously, understanding the risk. The naming and documentation make the security implications explicit — this is a deliberate choice, not an implicit vulnerability.

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: 54d0342e05

ℹ️ 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 Outdated
@openclaw-barnacle openclaw-barnacle Bot added the app: macos App: macos label Apr 3, 2026
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: ede127eae8

ℹ️ 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 Outdated
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: 82653dae77

ℹ️ 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/config/zod-schema.ts Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.

Comment thread src/gateway/auth.ts Outdated
Comment thread src/gateway/auth.ts Outdated
Comment thread src/gateway/auth.ts Outdated
Comment thread src/config/zod-schema.ts
Comment thread src/config/schema.labels.ts
Comment thread src/config/schema.help.ts
Comment thread src/config/types.gateway.ts Outdated
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: 08c61ac292

ℹ️ 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 Outdated
@Sandblaze05
Copy link
Copy Markdown
Author

Security + Contract Update

Summary

This PR hardens trusted-proxy auth for localhost reverse-proxy deployments while preserving existing observable failure semantics.

What changed

  1. Loopback trusted-proxy requests are now gated by:

    • explicit loopback trust in gateway.trustedProxies
    • a resolvable non-loopback forwarded client IP
    • proxy authenticity material (trustedProxy.authHeader + trustedProxy.authValue) for loopback path
  2. trustedProxy.authValue is marked sensitive in config schema/hints so redaction tooling treats it as a secret.

  3. Config validation now enforces trustedProxy.authHeader/trustedProxy.authValue pairing at parse time (schema-level), with field-specific errors. Whitespace-only values are rejected.

Contract compatibility (important)

Loopback-source rejections continue to return the existing reason code:

  • trusted_proxy_loopback_source

This preserves behavior for existing callers/consumers that rely on the previous loopback rejection contract, avoiding a reason-code migration in this PR.

Why this shape

  • Addresses localhost spoofing risk in trusted-proxy mode.
  • Keeps same-host reverse-proxy support.
  • Avoids breaking downstream reason-code consumers.

Validation

  • Focused gateway auth tests for loopback/proxy-auth flows pass.
  • Config schema tests for trusted-proxy parse-time constraints pass.
  • Generated schema baseline test passes.

@openclaw-barnacle openclaw-barnacle Bot added the docs Improvements or additions to documentation label Apr 3, 2026
@jeremyakers
Copy link
Copy Markdown

"The issue: The previous loopback exception allowed any local process to forge x-forwarded-for and identity headers, enabling privilege elevation attacks on same-host reverse proxies (nginx/Caddy on localhost)."

How does being non-local stop this form of attack? If I move my proxy to a separate host, a rogue local process on that host could still forge x-forwarded-for and identity headers... You're not removing any attack surface, you're just moving the surface to another host.

In practice blocking loopback proxies will make users far less secure because it pushes users toward binding Gateway on a non-loopback interface so a same-machine proxy can reach it via the host’s LAN IP, which can be less secure than a loopback-only deployment if firewalling is not perfect. The net effect may be to trade a localhost trust issue for broader network exposure.

Literally when I asked my OpenClaw jow to fix my proxy setup, it told me I would need to change my network bind to bind to "lan" (IE: 0.0.0.0) and use my hosts LAN IP (192.168.x.x) in the proxy config.

That is exactly what people are going to start doing.

@vincentkoc vincentkoc added the triage: dirty-candidate Candidate: broad unrelated surfaces; may need splitting or cleanup. label Apr 26, 2026
@vincentkoc
Copy link
Copy Markdown
Member

Thanks @Sandblaze05. #73266 landed the localhost trusted-proxy fix and closed #59167.

I am closing this PR as superseded because the landed version keeps the same-host reverse-proxy path behind an explicit gateway.auth.trustedProxy.allowLoopback opt-in, preserves requiredHeaders / allowUsers, and updates the security-audit/docs surface with that sharper contract.

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

Labels

docs Improvements or additions to documentation gateway Gateway runtime size: L triage: dirty-candidate Candidate: broad unrelated surfaces; may need splitting or cleanup.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Localhost trusted-proxy authentication broken

4 participants