diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index 44b8aaa579d..0fbac248fb2 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -2702,6 +2702,7 @@ See [Plugins](/tools/plugin). - `gateway.auth.mode: "trusted-proxy"`: delegate auth to an identity-aware reverse proxy and trust identity headers from `gateway.trustedProxies` (see [Trusted Proxy Auth](/gateway/trusted-proxy-auth)). This mode expects a **non-loopback** proxy source; same-host loopback reverse proxies do not satisfy trusted-proxy auth. - `gateway.auth.allowTailscale`: when `true`, Tailscale Serve identity headers can satisfy Control UI/WebSocket auth (verified via `tailscale whois`). HTTP API endpoints do **not** use that Tailscale header auth; they follow the gateway's normal HTTP auth mode instead. This tokenless flow assumes the gateway host is trusted. Defaults to `true` when `tailscale.mode = "serve"`. - `gateway.auth.rateLimit`: optional failed-auth limiter. Applies per client IP and per auth scope (shared-secret and device-token are tracked independently). Blocked attempts return `429` + `Retry-After`. + - On the async Tailscale Serve Control UI path, failed attempts for the same `{scope, clientIp}` are serialized before the failure write. Concurrent bad attempts from the same client can therefore trip the limiter on the second request instead of both racing through as plain mismatches. - `gateway.auth.rateLimit.exemptLoopback` defaults to `true`; set `false` when you intentionally want localhost traffic rate-limited too (for test setups or strict proxy deployments). - Browser-origin WS auth attempts are always throttled with loopback exemption disabled (defense-in-depth against browser-based localhost brute force). - On loopback, those browser-origin lockouts are isolated per normalized `Origin` diff --git a/docs/gateway/security/index.md b/docs/gateway/security/index.md index 9a7de586312..63732948b25 100644 --- a/docs/gateway/security/index.md +++ b/docs/gateway/security/index.md @@ -920,6 +920,10 @@ UI/WebSocket authentication. OpenClaw verifies the identity by resolving the and matching it to the header. This only triggers for requests that hit loopback and include `x-forwarded-for`, `x-forwarded-proto`, and `x-forwarded-host` as injected by Tailscale. +For this async identity check path, failed attempts for the same `{scope, ip}` +are serialized before the limiter records the failure. Concurrent bad retries +from one Serve client can therefore lock out the second attempt immediately +instead of racing through as two plain mismatches. HTTP API endpoints (for example `/v1/*`, `/tools/invoke`, and `/api/channels/*`) do **not** use Tailscale identity-header auth. They still follow the gateway's configured HTTP auth mode. diff --git a/docs/gateway/troubleshooting.md b/docs/gateway/troubleshooting.md index df51a9b5f33..8cd958097dc 100644 --- a/docs/gateway/troubleshooting.md +++ b/docs/gateway/troubleshooting.md @@ -120,6 +120,10 @@ Common signatures: - That cached-token retry reuses the cached scope set stored with the paired device token. Explicit `deviceToken` / explicit `scopes` callers keep their requested scope set instead. +- On the async Tailscale Serve Control UI path, failed attempts for the same + `{scope, ip}` are serialized before the limiter records the failure. Two bad + concurrent retries from the same client can therefore surface `retry later` + on the second attempt instead of two plain mismatches. - `too many failed authentication attempts (retry later)` from a browser-origin loopback client → repeated failures from that same normalized `Origin` are locked out temporarily; another localhost origin uses a separate bucket. diff --git a/docs/help/faq.md b/docs/help/faq.md index 83550e310d8..62024818826 100644 --- a/docs/help/faq.md +++ b/docs/help/faq.md @@ -168,6 +168,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS, **Not on localhost:** - **Tailscale Serve** (recommended): keep bind loopback, run `openclaw gateway --tailscale serve`, open `https:///`. If `gateway.auth.allowTailscale` is `true`, identity headers satisfy Control UI/WebSocket auth (no pasted shared secret, assumes trusted gateway host); HTTP APIs still require shared-secret auth unless you deliberately use private-ingress `none` or trusted-proxy HTTP auth. + Bad concurrent Serve auth attempts from the same client are serialized before the failed-auth limiter records them, so the second bad retry can already show `retry later`. - **Tailnet bind**: run `openclaw gateway --bind tailnet --token ""` (or configure password auth), open `http://:18789/`, then paste the matching shared secret in dashboard settings. - **Identity-aware reverse proxy**: keep the Gateway behind a non-loopback trusted proxy, configure `gateway.auth.mode: "trusted-proxy"`, then open the proxy URL. - **SSH tunnel**: `ssh -N -L 18789:127.0.0.1:18789 user@host` then open `http://127.0.0.1:18789/`. Shared-secret auth still applies over the tunnel; paste the configured token or password if prompted. diff --git a/docs/help/troubleshooting.md b/docs/help/troubleshooting.md index e9cdfbe6231..330d2d8ae93 100644 --- a/docs/help/troubleshooting.md +++ b/docs/help/troubleshooting.md @@ -142,6 +142,9 @@ flowchart TD - That cached-token retry reuses the cached scope set stored with the paired device token. Explicit `deviceToken` / explicit `scopes` callers keep their requested scope set instead. + - On the async Tailscale Serve Control UI path, failed attempts for the same + `{scope, ip}` are serialized before the limiter records the failure, so a + second concurrent bad retry can already show `retry later`. - `too many failed authentication attempts (retry later)` from a localhost browser origin → repeated failures from that same `Origin` are temporarily locked out; another localhost origin uses a separate bucket. diff --git a/docs/web/control-ui.md b/docs/web/control-ui.md index 18918e41e71..5b5943fd530 100644 --- a/docs/web/control-ui.md +++ b/docs/web/control-ui.md @@ -146,6 +146,10 @@ request hits loopback with Tailscale’s `x-forwarded-*` headers. Set `gateway.auth.allowTailscale: false` if you want to require explicit shared-secret credentials even for Serve traffic. Then use `gateway.auth.mode: "token"` or `"password"`. +For that async Serve identity path, failed auth attempts for the same client IP +and auth scope are serialized before rate-limit writes. Concurrent bad retries +from the same browser can therefore show `retry later` on the second request +instead of two plain mismatches racing in parallel. Tokenless Serve auth assumes the gateway host is trusted. If untrusted local code may run on that host, require token/password auth. diff --git a/docs/web/dashboard.md b/docs/web/dashboard.md index 33089311e68..cf0fc0fb3e7 100644 --- a/docs/web/dashboard.md +++ b/docs/web/dashboard.md @@ -77,6 +77,9 @@ Prefer localhost, Tailscale Serve, or an SSH tunnel. - Ensure the gateway is reachable (local: `openclaw status`; remote: SSH tunnel `ssh -N -L 18789:127.0.0.1:18789 user@host` then open `http://127.0.0.1:18789/`). - For `AUTH_TOKEN_MISMATCH`, clients may do one trusted retry with a cached device token when the gateway returns retry hints. That cached-token retry reuses the token's cached approved scopes; explicit `deviceToken` / explicit `scopes` callers keep their requested scope set. If auth still fails after that retry, resolve token drift manually. +- On the async Tailscale Serve Control UI path, failed attempts for the same + `{scope, ip}` are serialized before the failed-auth limiter records them, so + the second concurrent bad retry can already show `retry later`. - For token drift repair steps, follow [Token drift recovery checklist](/cli/devices#token-drift-recovery-checklist). - Retrieve or supply the shared secret from the gateway host: - Token: `openclaw config get gateway.auth.token`