Trusted Proxy Authentication for OpenClaw Gateway
Learn how to delegate Gateway authentication to a trusted reverse proxy like Pomerium, Caddy, or nginx with OAuth. This guide covers when to use it, security risks, and configuration steps.
Read this when
- Running OpenClaw behind an identity-aware proxy
- Setting up Pomerium, Caddy, or nginx with OAuth in front of OpenClaw
- Fixing WebSocket 1008 unauthorized errors with reverse proxy setups
- Deciding where to set HSTS and other HTTP hardening headers
Warning
Security-sensitive feature. This mode delegates authentication entirely to your reverse proxy. Misconfiguration can expose your Gateway to unauthorized access. Read this page carefully before enabling.
When to use
- OpenClaw sits behind an identity-aware proxy (Pomerium, Caddy + OAuth, nginx + oauth2-proxy, Traefik + forward auth).
- All authentication is handled by the proxy, which forwards user identity through headers.
- The Gateway lives in a Kubernetes or container setup where the proxy is the sole entry point.
- WebSocket
1008 unauthorizedfailures occur because browsers cannot include tokens in WS payloads.
When NOT to use
- The proxy does not authenticate users (it only terminates TLS or balances load).
- Any route to the Gateway exists that skips the proxy (open firewall ports, direct internal access).
- You cannot confirm that the proxy correctly strips or overwrites forwarded headers.
- You only require personal single-user access (Tailscale Serve with loopback is a better fit).
How it works
Proxy authenticates the user
Users are authenticated by your reverse proxy (via OAuth, OIDC, SAML, etc.).
Proxy adds an identity header
The proxy appends a header carrying the authenticated user's identity (for example, x-forwarded-user: nick@example.com).
Gateway verifies trusted source
OpenClaw verifies the request originates from a trusted proxy IP (gateway.trustedProxies) and is not the Gateway's own loopback or local interface address.
Gateway extracts identity
OpenClaw extracts the required headers, then pulls the user identity from the designated header.
Authorize
When all checks pass, and the user satisfies allowUsers (if configured), the request gains authorization.
Configuration
{
gateway: {
// Trusted-proxy auth expects the proxy's source IP to be non-loopback by default
bind: "lan",
// CRITICAL: Only add your proxy's IP(s) here
trustedProxies: ["10.0.0.1", "172.17.0.1"],
auth: {
mode: "trusted-proxy",
identityScopes: {
"admin@company.org": ["operator.admin"],
},
trustedProxy: {
// Header containing authenticated user identity (required)
userHeader: "x-forwarded-user",
// Optional: headers that MUST be present (proxy verification)
requiredHeaders: ["x-forwarded-proto", "x-forwarded-host"],
// Optional: restrict to specific users (empty = allow all)
allowUsers: ["nick@example.com", "admin@company.org"],
// Optional: allow a same-host loopback proxy after explicit opt-in
allowLoopback: false,
// Optional: let authenticated proxy users enroll new browser devices
deviceAutoApprove: {
enabled: false,
scopes: ["operator.read", "operator.write", "operator.approvals"],
},
},
},
},
}
Warning
Runtime rules, in order of evaluation
- Proxy-shaped traffic is attributed before Gateway auth. The request's source IP must match
gateway.trustedProxies(CIDR-aware), and its client-address headers must resolve to a non-loopback client. Otherwise Gateway-authenticated routes reject it withproxy_attribution_requiredbefore identity headers are accepted. Plugin-authenticated webhook routes may still handle the request, but they ignore the untrusted forwarded address and use the socket source for their own limits.- The proxy must overwrite
X-Forwarded-Forwith a safe chain. Ifgateway.allowRealIpFallback = true, an overwrittenX-Real-IPis also accepted whenX-Forwarded-Foris absent. Do not enable that fallback unless the proxy removes client-suppliedX-Real-IP.- Loopback-source requests (
127.0.0.1,::1) are rejected unlessgateway.auth.trustedProxy.allowLoopback = trueand the loopback address is also intrustedProxies(trusted_proxy_loopback_source). This check runs before header checks, so a loopback source fails this way even if required headers are also missing.- Non-loopback sources that match one of the Gateway host's own local network interface addresses are rejected as a spoofing guard (
trusted_proxy_local_interface_source). If interface discovery itself fails, the request is rejected too (trusted_proxy_local_interface_check_failed).requiredHeadersanduserHeadermust be present and non-blank.allowUsers, if non-empty, must include the extracted user.Forwarded-header evidence overrides loopback locality for local-direct fallback. If a request arrives on loopback but carries a
Forwarded, anyX-Forwarded-*, orX-Real-IPheader, that evidence disqualifies it from local-direct password fallback and device-identity gating, even though it still fails trusted-proxy auth as loopback.
allowLoopbacktrusts local processes on the Gateway host to the same degree as the reverse proxy. Enable it only when the Gateway is still firewalled from direct remote access and the local proxy strips or overwrites client-supplied identity headers.Internal Gateway clients that do not travel through the reverse proxy should use
gateway.auth.password/OPENCLAW_GATEWAY_PASSWORD, not trusted-proxy identity headers. Non-loopback Control UI deployments still need explicitgateway.controlUi.allowedOrigins.
Configuration reference
-
gateway.trustedProxies(string[], required), Array of proxy IP addresses (or CIDRs) to trust. Requests from other IPs are rejected. -
gateway.auth.mode(string, required), Must be"trusted-proxy". -
gateway.auth.identityScopes(true), "> Connection-only operator scopes granted to verified trusted-proxy or Tailscale identities. Email keys match case-insensitively; unknown scope names fail config validation. -
gateway.auth.trustedProxy.userHeader(string, required), Header name containing the authenticated user identity. -
gateway.auth.trustedProxy.requiredHeaders(string[]), Additional headers that must be present for the request to be trusted. -
gateway.auth.trustedProxy.allowUsers(string[]), Allowlist of user identities. Empty means allow all authenticated users. -
gateway.auth.trustedProxy.allowLoopback(boolean, default: false), Opt-in support for same-host loopback reverse proxies. -
gateway.auth.trustedProxy.deviceAutoApprove.enabled(boolean, default: false), Automatically approve new Control UI and WebChat device identities after trusted-proxy authentication. -
gateway.auth.trustedProxy.deviceAutoApprove.scopes(string[], default: ["operator.read", "operator.write", "operator.approvals"]), Maximum scopes granted to an auto-approved browser device. Explicitly listingoperator.adminlets every proxy-authenticated user request an automatic full-admin device grant, makes scope-less requests receive full admin automatically, and triggers the CRITICALgateway.trusted_proxy_device_auto_approve_adminsecurity audit finding plus a Gateway startup warning.
Warning
Only enable
allowLoopbackwhen the local reverse proxy is the intended trust boundary. Any local process that can connect to the Gateway can try to send proxy identity headers, so keep direct Gateway access private to the host and require proxy-owned headers such asx-forwarded-proto, or a signed assertion header where your proxy supports one.
Per-identity scope grants
Use gateway.auth.identityScopes to give selected verified users additional
operator scopes without widening their persistent device grant:
{
gateway: {
auth: {
mode: "trusted-proxy",
identityScopes: {
"admin@example.com": ["operator.admin"],
"operator@example.com": ["operator.read", "operator.write"],
},
trustedProxy: {
userHeader: "x-forwarded-user",
},
},
},
}
The map key is the verified trusted-proxy identity or Tailscale WhoIs login.
Email matching is case-insensitive; non-email identities match exactly. On each
connection, OpenClaw adds the matching identity scopes to the device-authorized
scopes, then applies an explicit x-openclaw-scopes connection cap.
These grants are session-only. They do not create or update device pairing records and do not trigger device scope-upgrade requests. Token, password, and no-auth connections do not carry a verified identity and never receive a grant.
Automatic device approval
Trusted-proxy auth can optionally use the proxy identity as the approval boundary for new browser devices:
{
gateway: {
auth: {
mode: "trusted-proxy",
trustedProxy: {
userHeader: "x-forwarded-user",
allowUsers: ["operator@example.com"],
deviceAutoApprove: {
enabled: true,
scopes: ["operator.read", "operator.write", "operator.approvals"],
},
},
},
},
}
The default is enabled: false. When enabled, all of these rules apply:
- The WebSocket must have been authenticated using the
trusted-proxymethod, carrying a non-empty user identity that satisfiedallowUserswhenever an allowlist is in place. This policy never applies to token, password, Tailscale, or unauthenticated connections. - Automatic approval is reserved for a fresh Control UI or WebChat browser device. Any request involving an existing device, even one asking for broader scopes, stays pending until manually approved with
openclaw devices approve <requestId>. - Approval assigns the device role
operator. When scopes accompany the connect request, the grant equals the precise overlap between those requested scopes anddeviceAutoApprove.scopes. If no scopes are sent, the configured list is granted, and when that list is absent, it falls back tooperator.read,operator.write, andoperator.approvals. The resulting grant is then further restricted by the connection'sx-openclaw-scopesproxy header if it exists, so a proxy that trims a user's scopes also constrains the persistent device grant, not merely the session, and a header that is present but empty results in zero scopes. This restriction applies even when the client sends no scope list of its own. operator.adminis permitted solely through an explicit entry indeviceAutoApprove.scopes. Once listed, any proxy-authenticated user can request and instantly obtain full admin on a new browser device, and requests that omit scopes are granted full admin automatically.openclaw security auditflags the CRITICALgateway.trusted_proxy_device_auto_approve_adminfinding, and the Gateway emits a one-time warning log at startup. When selected verified users need session-level admin without a persistent admin device grant, a targetedidentityScopesadmin grant is the preferred choice.
Warning
Turning this option on hands new browser device enrollment entirely to the reverse-proxy identity. A compromised proxy account can enroll a persistent device carrying every configured scope. Listing
operator.adminturns that device into a full administrator with no manual approval. Ensure the Gateway is reachable only via the proxy, enforce strong proxy authentication, overwrite identity headers, and keep theallowUserslist narrow.
Control UI pairing behavior
Browsers attach a device identity on every origin, including plain HTTP, so initial connects follow the standard pairing flow: automatic approval when deviceAutoApprove is enabled, otherwise a one-time approval on the Gateway host. When gateway.auth.mode = "trusted-proxy" is active and the request clears trusted-proxy checks, only Control UI sessions from browsers incapable of supplying any device identity are admitted without one.
Scope implications:
- Device-less Control UI WebSocket sessions cannot self-declare permissions. OpenClaw resets their requested scope list to
[], then applies any matching server-sideidentityScopesgrant after proxy identity verification. - If methods fail with
missing scopeafter a successful WebSocket connect, reload so the browser pairs its device identity, or approve the pending device request. See Control UI insecure HTTP.
Reverse-proxy scope capping: if your proxy sends x-openclaw-scopes on the Control UI WebSocket upgrade request, OpenClaw caps device enrollment or upgrade requests and the final union of device-authorized and identity-granted session scopes. This header never grants scopes; it only reduces authority. When deviceAutoApprove.enabled is true, the cap also constrains the persistent device grant written by automatic device approval.
Implications:
- Pairing no longer serves as the primary gate for device-less Control UI access. A matching
identityScopesentry can authorize that session without creating a pairing record. WhendeviceAutoApprove.enabledis true, the proxy identity also becomes the approval gate for new browser device enrollment. - Your reverse proxy auth policy and
allowUsersbecome the effective access control. - Keep gateway ingress locked to trusted proxy IPs only (
gateway.trustedProxies+ firewall).
Custom WebSocket clients are not Control UI sessions. The retired Control UI
upgrade input does not grant temporary access 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 with x-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
Behavior:
- 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.
Practical rule: send 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)
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.
Example header value:
Strict-Transport-Security: max-age=31536000; includeSubDomains
Gateway TLS termination
If OpenClaw itself serves HTTPS directly (no TLS-terminating proxy), set:
{
gateway: {
tls: { enabled: true },
http: {
securityHeaders: {
strictTransportSecurity: "max-age=31536000; includeSubDomains",
},
},
},
}
strictTransportSecurity accepts a string header value, or false to disable explicitly.
Rollout guidance
- Begin with a brief max age, such as
max-age=300, while you validate traffic. - Only after confidence builds should you extend to long-lived values, for example
max-age=31536000. - Add
includeSubDomainsonly when every subdomain is ready for HTTPS. - Opt into preload solely if you deliberately satisfy preload requirements across your entire domain set.
- HSTS provides no benefit for loopback-only local development.
Proxy setup examples
End-to-end coverage of Cloudflare Access, including tunnel and node routes, appears in Cloudflare Tunnel and Access.
Pomerium
Identity is passed by Pomerium through x-pomerium-claim-email (or other claim headers), with a JWT delivered via x-pomerium-jwt-assertion.
{
gateway: {
bind: "lan",
trustedProxies: ["10.0.0.1"], // Pomerium's IP
auth: {
mode: "trusted-proxy",
trustedProxy: {
userHeader: "x-pomerium-claim-email",
requiredHeaders: ["x-pomerium-jwt-assertion"],
},
},
},
}
Pomerium config snippet:
routes:
- from: https://openclaw.example.com
to: http://openclaw-gateway:18789
policy:
- allow:
or:
- email:
is: nick@example.com
pass_identity_headers: true
Caddy with OAuth
With the caddy-security plugin, Caddy can authenticate users and forward identity headers.
{
gateway: {
bind: "lan",
trustedProxies: ["10.0.0.1"], // Caddy/sidecar proxy IP
auth: {
mode: "trusted-proxy",
trustedProxy: {
userHeader: "x-forwarded-user",
},
},
},
}
Caddyfile snippet:
openclaw.example.com {
authenticate with oauth2_provider
authorize with policy1
reverse_proxy openclaw:18789 {
header_up X-Forwarded-User {http.auth.user.email}
}
}
nginx + oauth2-proxy
User authentication and identity passing via x-auth-request-email are handled by oauth2-proxy.
{
gateway: {
bind: "lan",
trustedProxies: ["10.0.0.1"], // nginx/oauth2-proxy IP
auth: {
mode: "trusted-proxy",
trustedProxy: {
userHeader: "x-auth-request-email",
},
},
},
}
nginx config snippet:
location / {
auth_request /oauth2/auth;
auth_request_set $user $upstream_http_x_auth_request_email;
proxy_pass http://openclaw:18789;
proxy_set_header X-Auth-Request-Email $user;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
Traefik with forward auth
{
gateway: {
bind: "lan",
trustedProxies: ["172.17.0.1"], // Traefik container IP
auth: {
mode: "trusted-proxy",
trustedProxy: {
userHeader: "x-forwarded-user",
},
},
},
}
Mixed token configuration
Gateway startup rejects trusted-proxy auth when a shared token is also present, whether via gateway.auth.token or OPENCLAW_GATEWAY_TOKEN. These two cannot coexist, because a shared token would permit same-host callers to authenticate through a route entirely separate from the proxy-verified identity this mode enforces.
Should startup fail with an error resembling gateway auth mode is trusted-proxy, but a shared token is also configured:
- In trusted-proxy mode, drop the shared token, or
- Move from
gateway.auth.modeto"token"if token-based auth is your goal.
Loopback trusted-proxy identity headers still fail closed: same-host callers are never silently authenticated as proxy users. Internal OpenClaw callers that skip the proxy can authenticate with gateway.auth.password / OPENCLAW_GATEWAY_PASSWORD instead. Token fallback stays intentionally unsupported in trusted-proxy mode.
Security checklist
Before turning on trusted-proxy auth, confirm:
- Proxy is the only path: Firewall the Gateway port so only your proxy can reach it.
- trustedProxies is minimal: List only your actual proxy IPs, not whole subnets.
- Loopback proxy source is deliberate: Requests from loopback fail closed under trusted-proxy auth unless
gateway.auth.trustedProxy.allowLoopbackis explicitly enabled for a same-host proxy. - Proxy strips headers: Your proxy overwrites, rather than appends,
x-forwarded-*headers coming from clients. - Client IP is attributable: The proxy consistently rebuilds
X-Forwarded-Forusing the original non-loopback client address. - TLS termination: TLS is handled by your proxy, so users connect over HTTPS.
- allowedOrigins is explicit: The non-loopback Control UI relies on explicit
gateway.controlUi.allowedOrigins. - allowUsers is set (recommended): Restrict access to known users instead of accepting anyone authenticated.
- No mixed token config: Avoid setting both
gateway.auth.tokenandgateway.auth.mode: "trusted-proxy". - Local password fallback is private: If
gateway.auth.passwordis configured for internal direct callers, keep the Gateway port firewalled so remote non-proxy clients cannot access it directly. - Device auto-approval is deliberate: When
deviceAutoApprove.enabledis true, treat reverse-proxy account security as the device-enrollment boundary and keep the granted scope list non-admin and minimal.
Security audit
A critical severity finding is flagged by openclaw security audit for trusted-proxy auth. That is by design, serving as a reminder that security is being delegated to your proxy setup.
The audit looks for:
- Base
gateway.trusted_proxy_authwarning/critical reminder. - Missing
trustedProxiesconfiguration. - Missing
userHeaderconfiguration. - Empty
allowUsers(permits any authenticated user). - Enabled
allowLoopbackfor same-host proxy sources. - Enabled browser device auto-approval (delegates new device pairing to the proxy identity).
Additional findings not specific to trusted-proxy also apply whenever Control UI is exposed: wildcard or missing gateway.controlUi.allowedOrigins, and Host-header origin fallback.
Troubleshooting
trusted_proxy_untrusted_source
The request originated from an IP outside gateway.trustedProxies. Verify:
- Is the proxy IP correct? (Docker container IPs may change.)
- Is a load balancer positioned in front of your proxy?
- Use
docker inspectorkubectl get pods -o wideto determine the actual IPs.
trusted_proxy_loopback_source
A loopback-source trusted-proxy request was rejected by OpenClaw.
Check:
- Is the proxy connecting from
127.0.0.1/::1? - Are you attempting trusted-proxy auth with a same-host loopback reverse proxy?
Fix:
- For internal same-host clients that bypass the proxy, prefer token/password auth, 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, retain the loopback address ingateway.trustedProxies, and ensure the proxy strips or overwrites identity headers.
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, guarding against spoofed same-host traffic on tailnets or Docker bridge networks. When ..._check_failed appears, interface discovery itself failed, so OpenClaw fails closed.
Check:
- Is a process on the Gateway host sending identity headers directly, bypassing the proxy?
- Does the proxy share the network namespace with the Gateway, using an IP that also appears as a local interface?
Fix: route proxy traffic through an address not bound locally by the Gateway host, or use allowLoopback only for a genuine same-host proxy setup.
trusted_proxy_user_missing
The user header came through blank or wasn't sent at all. Verify the following:
- Does your proxy forward identity headers?
- Is the header name spelled exactly right? (case doesn't matter, but the spelling does)
- Has the proxy actually authenticated the user?
trusted_proxy_missing_header_*
One of the required headers never arrived. Verify the following:
- That your proxy is set up to send those particular headers.
- That nothing in the chain is removing headers along the way.
trusted_proxy_user_not_allowed
The user passed authentication but isn't listed in allowUsers. Add them to it, or take the allowlist away.
trusted_proxy_no_proxies_configured / trusted_proxy_config_missing
gateway.auth.mode is set to "trusted-proxy", yet gateway.trustedProxies has no value, or gateway.auth.trustedProxy itself is absent. Until both are configured, every request gets denied.
trusted_proxy_origin_not_allowed
Trusted-proxy authentication went through, but the browser's Origin header didn't satisfy the Control UI's origin validation.
Check the following:
- That
gateway.controlUi.allowedOriginslists the exact origin the browser is using. - That you aren't depending on wildcard origins unless you truly want to permit everything.
- If you're deliberately using Host-header fallback mode, that
gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=truewas enabled on purpose.
Connection succeeds but methods report missing scope
The WebSocket establishes, but chat.history, sessions.list, or
models.list errors out with missing scope: operator.read.
Typical reasons:
- Control UI session without a device: OpenClaw strips self-declared scopes by design, and no matching
gateway.auth.identityScopesgrant was set up. - Custom backend client: the deprecated Control UI upgrade input never grants access to arbitrary backend or CLI-shaped WebSocket clients.
x-openclaw-scopestoo restrictive: if your proxy adds this header to the Control UI WebSocket upgrade request, session scopes are limited to that set. An empty header value means zero scopes.
To resolve:
- For Control UI, refresh the dashboard so the browser creates device identity and finishes pairing (this also works over HTTP).
- For custom automation, rely on device identity/pairing, the reserved direct-local
gateway-clientbackend helper path, or admin HTTP RPC. - Don't add the retired
gateway.controlUi.dangerouslyDisableDeviceAuthkey to current config; it's ignored, andopenclaw doctor --fixdeletes it.
WebSocket still failing
Confirm that your proxy:
- Handles WebSocket upgrades (
Upgrade: websocket,Connection: upgrade). - Forwards identity headers on WebSocket upgrade requests (not just HTTP).
- Doesn't use a different auth path for WebSocket connections.
Migration from token auth
Configure the proxy
Set up your proxy so it authenticates users and forwards headers.
Test the proxy independently
Validate the proxy setup on its own (curl with headers).
Update OpenClaw config
Adjust the OpenClaw config to enable trusted-proxy auth.
Restart the Gateway
Restart the Gateway.
Test WebSocket
Test WebSocket connections from the Control UI.
Audit
Execute openclaw security audit and go over the results.
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