Skip to content

fix(gateway): allow password fallback for trusted-proxy auth mode#64122

Closed
jetd1 wants to merge 28 commits intoopenclaw:mainfrom
jetd1:fix/trusted-proxy-password-fallback
Closed

fix(gateway): allow password fallback for trusted-proxy auth mode#64122
jetd1 wants to merge 28 commits intoopenclaw:mainfrom
jetd1:fix/trusted-proxy-password-fallback

Conversation

@jetd1
Copy link
Copy Markdown
Contributor

@jetd1 jetd1 commented Apr 10, 2026

Problem

When gateway.auth.mode is "trusted-proxy", the gateway WebSocket auth handler rejects all non-proxy connections (loopback, untrusted source) without checking password credentials. This breaks local clients — subagents, browser extensions, and CLI — that connect directly and cannot go through the reverse proxy.

PR #63280 (b1724f8b5f) auto-generates a browser control password for trusted-proxy mode and persists it to gateway.auth.password. However, the credential resolution path (call.ts, credential-planner.ts, credentials.ts) still blocks password reading for trusted-proxy mode, and the gateway auth dispatcher (auth.ts) still early-returns on trusted-proxy failure without attempting password verification.

Result: callGateway() never reads the password from config, and even if it did, the server would never check it. Subagent spawns, browser extension connections, and local CLI calls all fail with unauthorized.

This is reported in #17761, #26007, and #43300.

Root Cause

Two commits introduced conflicting assumptions:

Commit What it did Side effect
25252ab5ab (Mar 7, auth hardening) Made localAuthModeAllowsGatewaySecretInputPath() return false for trusted-proxy, blocking all secret resolution Client never reads password from config
b1724f8b5f (Apr 9, browser auto-token) Auto-generates and persists a 48-char hex password to gateway.auth.password in trusted-proxy mode Password exists in config but is inaccessible

The server-side authorizeGatewayConnectCore treats auth modes as mutually exclusive: trusted-proxy early-returns on failure, never falling back to password.

Solution

Make trusted-proxy + password work as complementary channels serving different client types, rather than mutually exclusive modes:

  • From trusted proxy IP → proxy header auth only (existing behavior, unchanged)
  • From loopback / non-proxy → password auth (new fallback path)

Changes

1. src/gateway/auth.tsauthorizeGatewayConnectCore

After the trusted-proxy path fails (untrusted source, loopback, missing header), attempt password verification if both the client and config provide one. The proxy path is tried first and remains preferred; password is only a second channel for local clients.

// Trusted-proxy path failed — attempt password fallback for local clients
if (auth.password && connectAuth?.password) {
  if (safeEqualSecret(connectAuth.password, auth.password)) {
    limiter?.reset(ip, rateLimitScope);
    return { ok: true, method: "password" };
  }
  limiter?.recordFailure(ip, rateLimitScope);
  return { ok: false, reason: "password_mismatch" };
}

2. src/gateway/call.tslocalAuthModeAllowsGatewaySecretInputPath

Allow password (but not token) secret input resolution for trusted-proxy mode. Token remains mutually exclusive with trusted-proxy; password does not.

if (authMode === "trusted-proxy") {
  return !isTokenGatewaySecretInputPath(path);
}

3. src/gateway/credential-planner.tspasswordCanWin

Add authMode === "trusted-proxy" to the passwordCanWin condition so the credential planner considers password a viable credential.

4. src/gateway/credentials.tslocalPasswordCanWin

Add params.plan.authMode === "trusted-proxy" to the localPasswordCanWin condition so local password resolution is allowed.

Security Properties Preserved

Property How it holds
External traffic still requires reverse proxy + identity header Password fallback only runs after the trusted-proxy path has already failed. External connections from untrusted IPs that lack the proxy headers fail at the proxy check and only reach the password check if they also provide a password — which they cannot obtain without filesystem access.
Password leakage = config file leakage (same trust boundary) The auto-generated password is stored in openclaw.json. Any attacker who can read it already has the same access level as a password-holding client.
No bypass of proxy auth by password from external sources The trusted-proxy path is tried first. Only when it fails AND the client presents a password does the fallback activate.
Rate limiting on wrong password password_mismatch records a rate-limit failure, preventing brute-force attacks.

Test Coverage

  • 7 new tests in auth.test.ts: password accepted from loopback, password accepted from untrusted source, wrong password rejected, no client password rejected, no gateway password rejected, trusted-proxy identity still preferred over password
  • 2 updated tests in call.test.ts: split it.each(["none", "trusted-proxy"]) into separate cases — none still ignores password, trusted-proxy now resolves it
  • 1 updated test in credentials.test.ts: trusted-proxy with unresolved password ref now throws instead of silently returning undefined

All 168 tests across auth.test.ts, call.test.ts, credentials.test.ts, and connection-auth.test.ts pass.

Closes #17761, #26007, #43300.

@jetd1 jetd1 requested a review from a team as a code owner April 10, 2026 05:48
@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime size: M labels Apr 10, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 10, 2026

Greptile Summary

This PR fixes a real breakage where trusted-proxy mode blocked all password-based auth, causing local clients (subagents, browser extensions, CLI) to fail with unauthorized even though a password was auto-generated and persisted by a prior commit. The solution adds a password fallback on the server side after the proxy path fails, and unblocks password resolution on the client credential chain.

  • The rate-limiter pre-check (limiter.check()) is not called before the password comparison in the new fallback block — recordFailure is wired up, but the lockout is never enforced on this path, contradicting the PR's stated security property.

Confidence Score: 4/5

Safe to merge after addressing the missing rate-limit pre-check in the trusted-proxy password fallback path.

The fix is architecturally sound and the credential-planner, credentials, and call.ts changes are all consistent and well-tested. One P1 gap: the rate-limiter lockout is never queried before the password comparison in the trusted-proxy fallback block, leaving the fallback path open to unlimited password retries even after the IP has been locked out. The password itself has high entropy (48-char hex), which reduces practical risk, but the missing check contradicts the PR's own stated security guarantee and should be addressed.

src/gateway/auth.ts — the trusted-proxy password fallback block (around line 561) needs a limiter.check() pre-check before the password comparison.

Security Review

  • Rate-limit pre-check bypassed on trusted-proxy password fallback (src/gateway/auth.ts line 561): limiter.check() is never called before the password comparison in the trusted-proxy fallback block because the check block is after the early-return that ends the trusted-proxy branch. recordFailure is called on wrong passwords, but existing lockouts are not enforced, allowing unlimited password retries on this path despite the PR's stated security guarantee.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/gateway/auth.ts
Line: 561-568

Comment:
**Rate-limit pre-check skipped on trusted-proxy password fallback**

The `limiter.check()` enforcement block lives at lines 577–587, after the early-return at the end of the `if (auth.mode === "trusted-proxy")` block, so it is never reached when this fallback fires. `recordFailure` is called on a wrong password, but the existing lockout state is never queried first — meaning an attacker can retry the password from a failed-proxy IP indefinitely without being blocked, even after the limiter has locked them out.

The PR description explicitly lists rate limiting as a security property ("Rate limiting on wrong password → `password_mismatch` records a rate-limit failure, preventing brute-force attacks"), but this guarantee doesn't hold without a `check()` call on the way in.

```typescript
    if (auth.password && connectAuth?.password) {
+     if (limiter) {
+       const rlCheck = limiter.check(ip, rateLimitScope);
+       if (!rlCheck.allowed) {
+         return { ok: false, reason: "rate_limited", rateLimited: true, retryAfterMs: rlCheck.retryAfterMs };
+       }
+     }
      if (safeEqualSecret(connectAuth.password, auth.password)) {
        limiter?.reset(ip, rateLimitScope);
        return { ok: true, method: "password" };
      }
      limiter?.recordFailure(ip, rateLimitScope);
      return { ok: false, reason: "password_mismatch" };
    }
```

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

Reviews (1): Last reviewed commit: "fix(gateway): allow password fallback fo..." | Re-trigger Greptile

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: 62bf4e7935

ℹ️ 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
@jetd1 jetd1 force-pushed the fix/trusted-proxy-password-fallback branch from 62bf4e7 to 3749ccb Compare April 10, 2026 05:58
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: 3749ccb588

ℹ️ 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
Comment thread src/gateway/credentials.ts Outdated
@jetd1 jetd1 force-pushed the fix/trusted-proxy-password-fallback branch 2 times, most recently from 83236e2 to a570e01 Compare April 10, 2026 06:27
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: a570e01b87

ℹ️ 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/call.ts Outdated
@jetd1 jetd1 force-pushed the fix/trusted-proxy-password-fallback branch from a570e01 to a67d8ca Compare April 10, 2026 06:50
@jetd1 jetd1 force-pushed the fix/trusted-proxy-password-fallback branch from a67d8ca to cccb1ff Compare April 18, 2026 10:08
@jetd1 jetd1 force-pushed the fix/trusted-proxy-password-fallback branch from cccb1ff to 6358423 Compare April 18, 2026 10:12
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: 63584239a5

ℹ️ 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
// same-host reverse proxy — which hits `trusted_proxy_loopback_source`
// while forwarding external traffic — cannot bypass proxy-header
// checks just by presenting the password.
if (isNonProxyFailure && localDirect && auth.password && connectAuth?.password) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Harden trusted-proxy fallback against loopback proxy traffic

This fallback gate relies on localDirect, which only means loopback socket + no forwarded headers; it does not prove the request originated on-host. A same-host reverse proxy that forwards external traffic without Forwarded/X-Forwarded-* headers will still satisfy localDirect, and because trusted_proxy_loopback_source is treated as a non-proxy failure, this branch can accept password auth and bypass trusted-proxy header/user policy. In publicly exposed same-host proxy deployments, that broadens password fallback from local-only to remotely reachable traffic.

Useful? React with 👍 / 👎.

@nickytonline
Copy link
Copy Markdown
Contributor

nickytonline commented Apr 19, 2026

I think this should be a first-class service identity flow, not a trusted-proxy fallback.

In trusted-proxy mode, internal OpenClaw calls (CLI/tools/subagents/backend) should authenticate as a service principal (service account), with explicit validation in the gateway. That’s clearer and more secure than inferring trust from loopback behaviour or missing proxy headers.

If proxy-derived identity is required for these calls, the WS/RPC path should use the configured proxy URL so auth behaviour stays consistent with the rest of trusted-proxy mode.

I appreciate the security hardening in #54536. But this PR + that work still feels like incremental exception handling. A dedicated service-account auth path would be cleaner, easier to reason about, and less fragile long-term.

And to remove friction using the service accoutn token, the CLI coul still accept a e.g. --service-account-token flag, but it would default to the one in the trusted-proxy auth config if one wasn't passed, same for the ws URL.

Thoughts?

steipete and others added 8 commits April 21, 2026 16:22
…dit (openclaw#69581)

* fix(codex): exclude codex-app-server synthetic apiKey from secrets audit

The Codex extension uses the literal string "codex-app-server" as a
hardcoded placeholder apiKey in provider.ts, since the real
authentication is managed by the app-server transport itself.

The secrets audit currently reports this as a real plaintext leak
(PLAINTEXT_FOUND), producing a false positive for any user who has
configured the Codex harness.

Declare it as a plugin-owned non-secret marker in the Codex plugin
manifest, so it flows through the standard
`listKnownNonSecretApiKeyMarkers()` path alongside `ollama-local`,
`lmstudio-local`, `gcp-vertex-credentials`, and `minimax-oauth`.

Also extends the existing `model auth markers` unit tests to lock
in the behavior.

Fixes openclaw#69511

* ci: retrigger checks (no-op)

(cherry picked from commit 081da17)
openclaw#69338)

When ingestDelta returns null (first empty/commentary delta or unchanged
content), the handler returned early, skipping setActivityStatus and
armStreamingWatchdog. If all subsequent deltas were also null (e.g.
due to phase filtering), the watchdog was never armed and the status bar
stayed stale as "idle" while a run was live.

Move setActivityStatus("streaming") and armStreamingWatchdog before
the null-displayText guard so they fire on every received delta event.

Fixes openclaw#34513, openclaw#40824

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 89b6d02)
@jetd1 jetd1 force-pushed the fix/trusted-proxy-password-fallback branch from 6358423 to c883dd9 Compare April 22, 2026 15:44
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: 13e0c530fd

ℹ️ 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 on lines +50 to +52
if (params.mode === "trusted-proxy") {
return !isTokenPath;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid forcing trusted-proxy password SecretRef resolution

This new trusted-proxy branch makes password SecretRefs mandatory to resolve even when password auth is only an optional fallback, which breaks flows that should still work without a resolved password. In particular, resolvePairingSetupFromConfig() calls materializeGatewayAuthSecretRefs() before choosing the auth label, so a config like gateway.auth.mode="trusted-proxy" plus an unavailable gateway.auth.password SecretRef now throws instead of allowing token-labeled pairing setup. That is a regression from optional fallback semantics and can block pairing/setup commands in trusted-proxy deployments with unresolved password refs.

Useful? React with 👍 / 👎.

@jetd1 jetd1 force-pushed the fix/trusted-proxy-password-fallback branch from 13e0c53 to cbe92c1 Compare April 22, 2026 17:26
@jetd1 jetd1 requested a review from a team as a code owner April 22, 2026 17:26
@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation channel: discord Channel integration: discord cli CLI command changes scripts Repository scripts docker Docker and sandbox tooling agents Agent runtime and tooling extensions: qa-lab extensions: codex labels Apr 22, 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: cbe92c146d

ℹ️ 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 on lines +50 to +51
if (params.mode === "trusted-proxy") {
return !isTokenPath;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep trusted-proxy password SecretRefs optional

shouldResolveGatewayAuthSecretRef now resolves gateway.auth.password for every trusted-proxy call (return !isTokenPath), so unresolved password SecretRefs become hard failures even when password is only an optional fallback. This regresses flows that can proceed without a password, e.g. resolvePairingSetupFromConfig() materializes auth SecretRefs before selecting the trusted-proxy auth label, so an unavailable password ref can abort pairing setup instead of cleanly continuing with token/bootstrap behavior.

Useful? React with 👍 / 👎.

@steipete
Copy link
Copy Markdown
Contributor

Thanks @jetd1. Closing this as superseded by the landed fix in #73034, which closes #17761.

The landed path keeps trusted-proxy credentials first, allows configured gateway password fallback only for local/same-host callers, keeps bearer-token fallback rejected, rate-limits fallback attempts, and includes docs/changelog plus regression coverage for the gateway and secret-surface behavior.

Canonical merge: 7975305

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

Labels

agents Agent runtime and tooling channel: discord Channel integration: discord cli CLI command changes docker Docker and sandbox tooling docs Improvements or additions to documentation extensions: codex extensions: qa-lab gateway Gateway runtime scripts Repository scripts 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]: Gateway auth dispatcher blocks all internal services when mode=trusted-proxy (no shared-secret fallback)

7 participants