When to use
- You run OpenClaw behind an identity-aware proxy (Pomerium, Caddy + OAuth, nginx + oauth2-proxy, Traefik + forward auth).
- Your proxy handles all authentication and passes user identity via headers.
- You’re in a Kubernetes or container environment where the proxy is the only path to the Gateway.
- You’re hitting WebSocket
1008 unauthorizederrors because browsers can’t pass tokens in WS payloads.
When NOT to use
- Your proxy doesn’t authenticate users (just a TLS terminator or load balancer).
- There’s any path to the Gateway that bypasses the proxy (firewall holes, internal network access).
- You’re unsure whether your proxy correctly strips/overwrites forwarded headers.
- You only need personal single-user access (consider Tailscale Serve + loopback instead).
How it works
Proxy adds an identity header
Proxy adds a header with the authenticated user identity (e.g.,
x-forwarded-user: nick@example.com).Gateway verifies trusted source
OpenClaw checks that the request came from a trusted proxy IP (
gateway.trustedProxies) and is not the Gateway’s own loopback or local interface address.Gateway extracts identity
OpenClaw reads the required headers, then the user identity from the configured header.
Configuration
Configuration reference
Array of proxy IP addresses (or CIDRs) to trust. Requests from other IPs are rejected.
Must be
"trusted-proxy".Header name containing the authenticated user identity.
Additional headers that must be present for the request to be trusted.
Allowlist of user identities. Empty means allow all authenticated users.
Opt-in support for same-host loopback reverse proxies.
Control UI pairing behavior
Whengateway.auth.mode = "trusted-proxy" is active and the request passes trusted-proxy checks, Control UI WebSocket sessions can connect without device pairing identity.
Scope implications:
- Device-less Control UI WebSocket sessions connect but receive no operator scopes by default. OpenClaw clears the requested scope list to
[]so a session not bound to an approved paired device/token cannot self-declare permissions. - If methods fail with
missing scopeafter a successful WebSocket connect, use HTTPS so the browser can generate device identity and complete pairing. See Control UI insecure HTTP. - Break-glass only:
gateway.controlUi.dangerouslyDisableDeviceAuth=truepreserves requested scopes even without device identity. This is a severe security downgrade; revert quickly. See Control UI insecure HTTP.
x-openclaw-scopes on the Control UI WebSocket upgrade request, OpenClaw caps the session scopes to the intersection of the requested scopes and the declared scopes. This header does not grant scopes; it only narrows what the session can hold.
Implications:
- Pairing is no longer the primary gate for Control UI access in this mode.
- Your reverse proxy auth policy and
allowUsersbecome the effective access control. - Keep gateway ingress locked to trusted proxy IPs only (
gateway.trustedProxies+ firewall).
gateway.controlUi.dangerouslyDisableDeviceAuth does not grant scopes to arbitrary client.mode: "backend" or CLI-shaped clients. Custom automation should use device identity/pairing, the reserved direct-local client.id: "gateway-client" backend helper path, or the admin HTTP RPC plugin when an HTTP request/response surface is a better fit.
Operator scopes header
Trusted-proxy auth is an identity-bearing HTTP mode, so callers may optionally declare operator scopes withx-openclaw-scopes on HTTP API requests.
Note: WebSocket scopes are determined by the Gateway protocol handshake and device identity binding. On Control UI WebSocket upgrade requests, x-openclaw-scopes is only a cap on the negotiated session scopes, not a grant. See Control UI pairing behavior.
Examples:
x-openclaw-scopes: operator.readx-openclaw-scopes: operator.read,operator.writex-openclaw-scopes: operator.admin,operator.write
- When the header is present, OpenClaw honors the declared scope set.
- When the header is present but empty, the request declares no operator scopes.
- When the header is absent, normal identity-bearing HTTP APIs fall back to the standard operator default scope set (
operator.admin,operator.read,operator.write,operator.approvals,operator.pairing,operator.talk.secrets). - Gateway-auth plugin HTTP routes are narrower by default: when
x-openclaw-scopesis absent, their runtime scope falls back tooperator.writeonly. - Browser-origin HTTP requests still have to pass
gateway.controlUi.allowedOrigins(or deliberate Host-header fallback mode) even after trusted-proxy auth succeeds.
x-openclaw-scopes explicitly when you want a trusted-proxy request to be narrower than the defaults, or when a gateway-auth plugin route needs something stronger than write scope.
TLS termination and HSTS
Use one TLS termination point and apply HSTS there.- Proxy TLS termination (recommended)
- Gateway TLS termination
When your reverse proxy handles HTTPS for
https://control.example.com, set Strict-Transport-Security at the proxy for that domain.- Good fit for internet-facing deployments.
- Keeps certificate + HTTP hardening policy in one place.
- OpenClaw can stay on loopback HTTP behind the proxy.
Rollout guidance
- Start with a short max age first (for example
max-age=300) while validating traffic. - Increase to long-lived values (for example
max-age=31536000) only after confidence is high. - Add
includeSubDomainsonly if every subdomain is HTTPS-ready. - Use preload only if you intentionally meet preload requirements for your full domain set.
- Loopback-only local development does not benefit from HSTS.
Proxy setup examples
Pomerium
Pomerium
Pomerium passes identity in Pomerium config snippet:
x-pomerium-claim-email (or other claim headers) and a JWT in x-pomerium-jwt-assertion.Caddy with OAuth
Caddy with OAuth
Caddy with the Caddyfile snippet:
caddy-security plugin can authenticate users and pass identity headers.nginx + oauth2-proxy
nginx + oauth2-proxy
oauth2-proxy authenticates users and passes identity in nginx config snippet:
x-auth-request-email.Traefik with forward auth
Traefik with forward auth
Mixed token configuration
Gateway startup rejects trusted-proxy auth if a shared token is also configured (gateway.auth.token or OPENCLAW_GATEWAY_TOKEN). The two are mutually exclusive because a shared token would let same-host callers authenticate on a completely different path than the proxy-verified identity this mode is meant to enforce.
If startup fails with an error like gateway auth mode is trusted-proxy, but a shared token is also configured:
- Remove the shared token when using trusted-proxy mode, or
- Switch
gateway.auth.modeto"token"if you intend token-based auth.
gateway.auth.password / OPENCLAW_GATEWAY_PASSWORD instead. Token fallback remains intentionally unsupported in trusted-proxy mode.
Security checklist
Before enabling trusted-proxy auth, verify:- Proxy is the only path: The Gateway port is firewalled from everything except your proxy.
- trustedProxies is minimal: Only your actual proxy IPs, not entire subnets.
- Loopback proxy source is deliberate: trusted-proxy auth fails closed for loopback-source requests unless
gateway.auth.trustedProxy.allowLoopbackis explicitly enabled for a same-host proxy. - Proxy strips headers: Your proxy overwrites (not appends)
x-forwarded-*headers from clients. - TLS termination: Your proxy handles TLS; users connect via HTTPS.
- allowedOrigins is explicit: Non-loopback Control UI uses explicit
gateway.controlUi.allowedOrigins. - allowUsers is set (recommended): Restrict to known users rather than allowing anyone authenticated.
- No mixed token config: Do not set both
gateway.auth.tokenandgateway.auth.mode: "trusted-proxy". - Local password fallback is private: If you configure
gateway.auth.passwordfor internal direct callers, keep the Gateway port firewalled so non-proxy remote clients cannot reach it directly.
Security audit
openclaw security audit flags trusted-proxy auth with a critical severity finding. This is intentional; it’s a reminder that you’re delegating security to your proxy setup.
The audit checks for:
- Base
gateway.trusted_proxy_authwarning/critical reminder. - Missing
trustedProxiesconfiguration. - Missing
userHeaderconfiguration. - Empty
allowUsers(allows any authenticated user). - Enabled
allowLoopbackfor same-host proxy sources.
gateway.controlUi.allowedOrigins, and Host-header origin fallback.
Troubleshooting
trusted_proxy_untrusted_source
trusted_proxy_untrusted_source
The request didn’t come from an IP in
gateway.trustedProxies. Check:- Is the proxy IP correct? (Docker container IPs can change.)
- Is there a load balancer in front of your proxy?
- Use
docker inspectorkubectl get pods -o wideto find actual IPs.
trusted_proxy_loopback_source
trusted_proxy_loopback_source
OpenClaw rejected a loopback-source trusted-proxy request.Check:
- Is the proxy connecting from
127.0.0.1/::1? - Are you trying to use trusted-proxy auth with a same-host loopback reverse proxy?
- Prefer token/password auth for internal same-host clients that do not go through the proxy, or
- Route through a non-loopback trusted proxy address and keep that IP in
gateway.trustedProxies, or - For a deliberate same-host reverse proxy, set
gateway.auth.trustedProxy.allowLoopback = true, keep the loopback address ingateway.trustedProxies, and make sure the proxy strips or overwrites identity headers.
trusted_proxy_local_interface_source / trusted_proxy_local_interface_check_failed
trusted_proxy_local_interface_source / trusted_proxy_local_interface_check_failed
The request’s source IP matched one of the Gateway host’s own non-loopback network interface addresses (not the proxy), a guard against spoofed same-host traffic on tailnets or Docker bridge networks.
..._check_failed means interface discovery itself errored, so OpenClaw fails closed.Check:- Is a process on the Gateway host itself sending identity headers directly, bypassing the proxy?
- Does the proxy run in the same network namespace as the Gateway, with an IP that also shows up as a local interface?
allowLoopback only for a genuine same-host proxy setup.trusted_proxy_user_missing
trusted_proxy_user_missing
The user header was empty or missing. Check:
- Is your proxy configured to pass identity headers?
- Is the header name correct? (case-insensitive, but spelling matters)
- Is the user actually authenticated at the proxy?
trusted_proxy_missing_header_*
trusted_proxy_missing_header_*
A required header wasn’t present. Check:
- Your proxy configuration for those specific headers.
- Whether headers are being stripped somewhere in the chain.
trusted_proxy_user_not_allowed
trusted_proxy_user_not_allowed
The user is authenticated but not in
allowUsers. Either add them or remove the allowlist.trusted_proxy_no_proxies_configured / trusted_proxy_config_missing
trusted_proxy_no_proxies_configured / trusted_proxy_config_missing
gateway.auth.mode is "trusted-proxy" but gateway.trustedProxies is empty, or gateway.auth.trustedProxy itself is missing. Every request is rejected until both are set.trusted_proxy_origin_not_allowed
trusted_proxy_origin_not_allowed
Trusted-proxy auth succeeded, but the browser
Origin header did not pass Control UI origin checks.Check:gateway.controlUi.allowedOriginsincludes the exact browser origin.- You are not relying on wildcard origins unless you intentionally want allow-all behavior.
- If you intentionally use Host-header fallback mode,
gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=trueis set deliberately.
Connection succeeds but methods report missing scope
Connection succeeds but methods report missing scope
The WebSocket connects, but
chat.history, sessions.list, or
models.list fails with missing scope: operator.read.Common causes:- Device-less Control UI session: trusted-proxy auth can admit the WebSocket connection without device identity, but OpenClaw clears scopes on device-less sessions by design.
- Custom backend client:
gateway.controlUi.dangerouslyDisableDeviceAuthis Control UI scoped and does not grant scopes to arbitrary backend or CLI-shaped WebSocket clients. - Overly narrow
x-openclaw-scopes: if your proxy injects this header on the Control UI WebSocket upgrade request, the session scopes are capped to that set. An empty header value yields no scopes.
- For Control UI, use HTTPS so the browser can generate device identity and complete pairing.
- For custom automation, use device identity/pairing, the reserved direct-local
gateway-clientbackend helper path, or admin HTTP RPC. - Use
gateway.controlUi.dangerouslyDisableDeviceAuth: trueonly as a temporary Control UI break-glass path.
WebSocket still failing
WebSocket still failing
Make sure your proxy:
- Supports WebSocket upgrades (
Upgrade: websocket,Connection: upgrade). - Passes the identity headers on WebSocket upgrade requests (not just HTTP).
- Doesn’t have a separate auth path for WebSocket connections.
Migration from token auth
Related
- Configuration — config reference
- Operator scopes — roles, scopes, and approval checks
- Remote access — other remote access patterns
- Security — full security guide
- Tailscale — simpler alternative for tailnet-only access