Skip to content

Gateway: trusted-proxy mode + allowLoopback=true rejects internal loopback callers despite valid gateway.auth.password (docs say fallback should work) #82607

Description

@esqandil

Summary

In gateway.auth.mode: "trusted-proxy" with allowLoopback: true, internal loopback clients that present a valid gateway.auth.password are rejected with trusted_proxy_missing_header_* before the documented "local direct fallback" path runs. The docs explicitly promise this path works; the runtime contradicts them.

Repro on 2026.5.12 (f066dd2, Node v22.22.2, Linux 6.8.0).

Expected (per docs)

docs/gateway/configuration-reference.md line 518:

Internal same-host callers can use gateway.auth.password as a local direct fallback; gateway.auth.token remains mutually exclusive with trusted-proxy mode.

docs/gateway/trusted-proxy-auth.md line 308:

Loopback trusted-proxy identity headers still fail closed: same-host callers are not silently authenticated as proxy users. Internal OpenClaw callers that bypass the proxy may authenticate with gateway.auth.password / OPENCLAW_GATEWAY_PASSWORD instead.

Same doc, line 101:

Forwarded-header evidence overrides loopback locality for local direct fallback. If a request arrives on loopback but carries X-Forwarded-For / X-Forwarded-Host / X-Forwarded-Proto headers pointing at a non-local origin, that evidence disqualifies local-direct password fallback…

Read together: a loopback connect with no forwarded headers and a valid auth.password should succeed via local-direct fallback even when auth.mode = "trusted-proxy".

Actual

Every internal loopback connect (backend RPC, CLI, subagent spawn, plugin approval channel) is rejected. The rejection log fields:

authMode=trusted-proxy
authProvided=password
authReason=trusted_proxy_missing_header_cf-access-jwt-assertion
client=gateway-client mode=backend hasDeviceIdentity=false
peer=127.0.0.1:44422->127.0.0.1:18789
fwd=n/a origin=n/a host=127.0.0.1:18789 ua=n/a

Note specifically:

  • authProvided=password — the client did send the configured password.
  • fwd=n/a — there are no X-Forwarded-* headers, so the "forwarded-header evidence overrides loopback locality" carve-out does NOT apply.
  • authReason=trusted_proxy_missing_header_* — the rejection comes from the trusted-proxy required-headers check.

CLI calls with hasDeviceIdentity=true (paired) fail the same way, so it isn't a device-identity issue.

Config (minimised)

{
  "gateway": {
    "mode": "local",
    "auth": {
      "mode": "trusted-proxy",
      "trustedProxy": {
        "userHeader": "cf-access-authenticated-user-email",
        "requiredHeaders": [
          "cf-access-jwt-assertion",
          "x-forwarded-host",
          "x-forwarded-proto"
        ],
        "allowUsers": ["redacted"],
        "allowLoopback": true
      },
      "password": "<32-byte hex>"
    },
    "trustedProxies": ["127.0.0.1/32", "::1/128"]
  }
}

Plus OPENCLAW_GATEWAY_PASSWORD=<same value> exported into the gateway process via systemd EnvironmentFile=. Confirmed via /proc/<gateway-pid>/environ.

No gateway.auth.token set anywhere. No OPENCLAW_GATEWAY_TOKEN. No mixed-token startup error.

Setup context

Same-host cloudflared sits in front, terminating Cloudflare Access at nemoclaw.<host>.com and forwarding to 127.0.0.1:18789 with the four CF headers. Browser flow through cloudflared works. Only internal loopback callers (no CF headers) fail.

Auth pipeline order suspicion

The runtime appears to run trusted-proxy requiredHeaders validation before local-direct password fallback for all loopback connections. The authProvided=password field shows the password check would have succeeded; the trusted-proxy header check fires first and short-circuits the reply with trusted_proxy_missing_header_*. Per the docs, the order should be reversed (or at least: no-forwarded-headers loopback + password-supplied → password fallback wins).

Impact

  • sessions_spawn rejects with gateway closed (1008): unauthorized.
  • openclaw doctor, openclaw config get, and any CLI that opens a backend WS fails.
  • Plugin approval channel can't be reached, so any tool call that would prompt for approval (e.g. R7/R8 gates) instead errors with Plugin approval required (gateway unavailable).
  • Cloudflare Tunnel browser access is unaffected (those requests carry the CF headers and pass).

Minimal repro

  1. Configure gateway.auth.mode: "trusted-proxy" with allowLoopback: true and any non-trivial requiredHeaders.
  2. Set gateway.auth.password (and optionally OPENCLAW_GATEWAY_PASSWORD env).
  3. Start gateway.
  4. From the same host, run any CLI that opens a backend WS, e.g. openclaw config get gateway.auth.mode.

Expected: succeeds via password fallback.
Actual: gateway closed (1008): unauthorized, log shows authProvided=password authReason=trusted_proxy_missing_header_*.

Workarounds attempted

  • Setting OPENCLAW_GATEWAY_PASSWORD env var on the gateway process — no effect (password was already in config; this confirmed env path was wired).
  • openclaw doctor — same failure.
  • Multiple gateway restarts (SIGUSR1 + full systemctl restart) — same failure.
  • Service reinstall via openclaw service install — same failure.

Suggested fix direction

In the WS connect auth pipeline, when auth.mode = "trusted-proxy" and the inbound request is from loopback and carries no X-Forwarded-* evidence, route the request through the local-direct password path before running requiredHeaders validation. The current order seems to always run requiredHeaders first, which negates the documented fallback.

Alternative: make the fallback order explicit/configurable so this setup can be expressed without ambiguity.

Environment

  • OpenClaw 2026.5.12 (f066dd2)
  • Node v22.22.2
  • Linux 6.8.0-111-generic x64
  • systemd user service (openclaw-gateway.service)
  • Loopback bind, same-host cloudflared front-door

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions