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 unauthorized failures 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

  1. 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 with proxy_attribution_required before 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.
  2. The proxy must overwrite X-Forwarded-For with a safe chain. If gateway.allowRealIpFallback = true, an overwritten X-Real-IP is also accepted when X-Forwarded-For is absent. Do not enable that fallback unless the proxy removes client-supplied X-Real-IP.
  3. Loopback-source requests (127.0.0.1, ::1) are rejected unless gateway.auth.trustedProxy.allowLoopback = true and the loopback address is also in trustedProxies (trusted_proxy_loopback_source). This check runs before header checks, so a loopback source fails this way even if required headers are also missing.
  4. 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).
  5. requiredHeaders and userHeader must be present and non-blank.
  6. 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, any X-Forwarded-*, or X-Real-IP header, that evidence disqualifies it from local-direct password fallback and device-identity gating, even though it still fails trusted-proxy auth as loopback.

allowLoopback trusts 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 explicit gateway.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 listing operator.admin lets every proxy-authenticated user request an automatic full-admin device grant, makes scope-less requests receive full admin automatically, and triggers the CRITICAL gateway.trusted_proxy_device_auto_approve_admin security audit finding plus a Gateway startup warning.

Warning

Only enable allowLoopback when 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 as x-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:

  1. The WebSocket must have been authenticated using the trusted-proxy method, carrying a non-empty user identity that satisfied allowUsers whenever an allowlist is in place. This policy never applies to token, password, Tailscale, or unauthenticated connections.
  2. 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>.
  3. Approval assigns the device role operator. When scopes accompany the connect request, the grant equals the precise overlap between those requested scopes and deviceAutoApprove.scopes. If no scopes are sent, the configured list is granted, and when that list is absent, it falls back to operator.read, operator.write, and operator.approvals. The resulting grant is then further restricted by the connection's x-openclaw-scopes proxy 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.
  4. operator.admin is permitted solely through an explicit entry in deviceAutoApprove.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 audit flags the CRITICAL gateway.trusted_proxy_device_auto_approve_admin finding, 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 targeted identityScopes admin 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.admin turns 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 the allowUsers list 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-side identityScopes grant after proxy identity verification.
  • If methods fail with missing scope after 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 identityScopes entry can authorize that session without creating a pairing record. When deviceAutoApprove.enabled is true, the proxy identity also becomes the approval gate for new browser device enrollment.
  • Your reverse proxy auth policy and allowUsers become 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.read
  • x-openclaw-scopes: operator.read,operator.write
  • x-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-scopes is absent, their runtime scope falls back to operator.write only.
  • 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 includeSubDomains only 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.mode to "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.allowLoopback is 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-For using 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.token and gateway.auth.mode: "trusted-proxy".
  • Local password fallback is private: If gateway.auth.password is 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.enabled is 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_auth warning/critical reminder.
  • Missing trustedProxies configuration.
  • Missing userHeader configuration.
  • Empty allowUsers (permits any authenticated user).
  • Enabled allowLoopback for 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 inspect or kubectl get pods -o wide to 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 in gateway.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.allowedOrigins lists 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=true was 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.identityScopes grant 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-scopes too 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-client backend helper path, or admin HTTP RPC.
  • Don't add the retired gateway.controlUi.dangerouslyDisableDeviceAuth key to current config; it's ignored, and openclaw doctor --fix deletes 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.

3,536 words · updated Aug 25, 2026