diff --git a/docs/gateway/trusted-proxy-auth.md b/docs/gateway/trusted-proxy-auth.md
index fb9f00b9df6..4776d8d7678 100644
--- a/docs/gateway/trusted-proxy-auth.md
+++ b/docs/gateway/trusted-proxy-auth.md
@@ -1,6 +1,7 @@
---
summary: "Delegate gateway authentication to a trusted reverse proxy (Pomerium, Caddy, nginx + OAuth)"
title: "Trusted proxy auth"
+sidebarTitle: "Trusted proxy auth"
read_when:
- Running OpenClaw behind an identity-aware proxy
- Setting up Pomerium, Caddy, or nginx with OAuth in front of OpenClaw
@@ -8,37 +9,49 @@ read_when:
- Deciding where to set HSTS and other HTTP hardening headers
---
-> ⚠️ **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.
+
+**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
+## When to use
Use `trusted-proxy` auth mode when:
-- 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 unauthorized` errors because browsers can't pass tokens in WS payloads
+- 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 unauthorized` errors because browsers can't pass tokens in WS payloads.
-## When NOT to Use
+## When NOT to use
-- If your proxy doesn't authenticate users (just a TLS terminator or load balancer)
-- If there's any path to the Gateway that bypasses the proxy (firewall holes, internal network access)
-- If you're unsure whether your proxy correctly strips/overwrites forwarded headers
-- If you only need personal single-user access (consider Tailscale Serve + loopback for simpler setup)
+- If your proxy doesn't authenticate users (just a TLS terminator or load balancer).
+- If there's any path to the Gateway that bypasses the proxy (firewall holes, internal network access).
+- If you're unsure whether your proxy correctly strips/overwrites forwarded headers.
+- If you only need personal single-user access (consider Tailscale Serve + loopback for simpler setup).
-## How It Works
+## How it works
-1. Your reverse proxy authenticates users (OAuth, OIDC, SAML, etc.)
-2. Proxy adds a header with the authenticated user identity (e.g., `x-forwarded-user: nick@example.com`)
-3. OpenClaw checks that the request came from a **trusted proxy IP** (configured in `gateway.trustedProxies`)
-4. OpenClaw extracts the user identity from the configured header
-5. If everything checks out, the request is authorized
+
+
+ Your reverse proxy authenticates users (OAuth, OIDC, SAML, etc.).
+
+
+ Proxy adds a header with the authenticated user identity (e.g., `x-forwarded-user: nick@example.com`).
+
+
+ OpenClaw checks that the request came from a **trusted proxy IP** (configured in `gateway.trustedProxies`).
+
+
+ OpenClaw extracts the user identity from the configured header.
+
+
+ If everything checks out, the request is authorized.
+
+
-## Control UI Pairing Behavior
+## Control UI pairing behavior
-When `gateway.auth.mode = "trusted-proxy"` is active and the request passes
-trusted-proxy checks, Control UI WebSocket sessions can connect without device
-pairing identity.
+When `gateway.auth.mode = "trusted-proxy"` is active and the request passes trusted-proxy checks, Control UI WebSocket sessions can connect without device pairing identity.
Implications:
@@ -74,61 +87,73 @@ Implications:
}
```
-Important runtime rule:
+
+**Important runtime rules**
- Trusted-proxy auth rejects loopback-source requests (`127.0.0.1`, `::1`, loopback CIDRs).
- Same-host loopback reverse proxies do **not** satisfy trusted-proxy auth.
- For same-host loopback proxy setups, use token/password auth instead, or route through a non-loopback trusted proxy address that OpenClaw can verify.
- Non-loopback Control UI deployments still need explicit `gateway.controlUi.allowedOrigins`.
- **Forwarded-header evidence overrides loopback locality.** If a request arrives on loopback but carries `X-Forwarded-For` / `X-Forwarded-Host` / `X-Forwarded-Proto` headers pointing at a non-local origin, that evidence disqualifies the loopback locality claim. The request is treated as remote for pairing, trusted-proxy auth, and Control UI device-identity gating. This prevents a same-host loopback proxy from laundering forwarded-header identity into trusted-proxy auth.
+
-### Configuration Reference
+### Configuration reference
-| Field | Required | Description |
-| ------------------------------------------- | -------- | --------------------------------------------------------------------------- |
-| `gateway.trustedProxies` | Yes | Array of proxy IP addresses to trust. Requests from other IPs are rejected. |
-| `gateway.auth.mode` | Yes | Must be `"trusted-proxy"` |
-| `gateway.auth.trustedProxy.userHeader` | Yes | Header name containing the authenticated user identity |
-| `gateway.auth.trustedProxy.requiredHeaders` | No | Additional headers that must be present for the request to be trusted |
-| `gateway.auth.trustedProxy.allowUsers` | No | Allowlist of user identities. Empty means allow all authenticated users. |
+
+ Array of proxy IP addresses 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.
+
## TLS termination and HSTS
Use one TLS termination point and apply HSTS there.
-### Recommended pattern: proxy TLS termination
+
+
+ When your reverse proxy handles HTTPS for `https://control.example.com`, set `Strict-Transport-Security` at the proxy for that domain.
-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.
-- 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:
-Example header value:
+ ```text
+ Strict-Transport-Security: max-age=31536000; includeSubDomains
+ ```
-```text
-Strict-Transport-Security: max-age=31536000; includeSubDomains
-```
+
+
+ If OpenClaw itself serves HTTPS directly (no TLS-terminating proxy), set:
-### Gateway TLS termination
-
-If OpenClaw itself serves HTTPS directly (no TLS-terminating proxy), set:
-
-```json5
-{
- gateway: {
- tls: { enabled: true },
- http: {
- securityHeaders: {
- strictTransportSecurity: "max-age=31536000; includeSubDomains",
+ ```json5
+ {
+ gateway: {
+ tls: { enabled: true },
+ http: {
+ securityHeaders: {
+ strictTransportSecurity: "max-age=31536000; includeSubDomains",
+ },
+ },
},
- },
- },
-}
-```
+ }
+ ```
-`strictTransportSecurity` accepts a string header value, or `false` to disable explicitly.
+ `strictTransportSecurity` accepts a string header value, or `false` to disable explicitly.
+
+
+
### Rollout guidance
@@ -138,124 +163,126 @@ If OpenClaw itself serves HTTPS directly (no TLS-terminating proxy), set:
- 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
+## Proxy setup examples
-### Pomerium
+
+
+ Pomerium passes identity in `x-pomerium-claim-email` (or other claim headers) and a JWT in `x-pomerium-jwt-assertion`.
-Pomerium passes identity in `x-pomerium-claim-email` (or other claim headers) and a JWT in `x-pomerium-jwt-assertion`.
-
-```json5
-{
- 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"],
+ ```json5
+ {
+ 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:
-
-```yaml
-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
-
-Caddy with the `caddy-security` plugin can authenticate users and pass identity headers.
-
-```json5
-{
- 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
+ Pomerium config snippet:
-oauth2-proxy authenticates users and passes identity in `x-auth-request-email`.
+ ```yaml
+ routes:
+ - from: https://openclaw.example.com
+ to: http://openclaw-gateway:18789
+ policy:
+ - allow:
+ or:
+ - email:
+ is: nick@example.com
+ pass_identity_headers: true
+ ```
-```json5
-{
- gateway: {
- bind: "lan",
- trustedProxies: ["10.0.0.1"], // nginx/oauth2-proxy IP
- auth: {
- mode: "trusted-proxy",
- trustedProxy: {
- userHeader: "x-auth-request-email",
+
+
+ Caddy with the `caddy-security` plugin can authenticate users and pass identity headers.
+
+ ```json5
+ {
+ gateway: {
+ bind: "lan",
+ trustedProxies: ["10.0.0.1"], // Caddy/sidecar proxy IP
+ auth: {
+ mode: "trusted-proxy",
+ trustedProxy: {
+ userHeader: "x-forwarded-user",
+ },
+ },
},
- },
- },
-}
-```
+ }
+ ```
-nginx config snippet:
+ Caddyfile snippet:
-```nginx
-location / {
- auth_request /oauth2/auth;
- auth_request_set $user $upstream_http_x_auth_request_email;
+ ```
+ openclaw.example.com {
+ authenticate with oauth2_provider
+ authorize with policy1
- 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";
-}
-```
+ reverse_proxy openclaw:18789 {
+ header_up X-Forwarded-User {http.auth.user.email}
+ }
+ }
+ ```
-### Traefik with Forward Auth
+
+
+ oauth2-proxy authenticates users and passes identity in `x-auth-request-email`.
-```json5
-{
- gateway: {
- bind: "lan",
- trustedProxies: ["172.17.0.1"], // Traefik container IP
- auth: {
- mode: "trusted-proxy",
- trustedProxy: {
- userHeader: "x-forwarded-user",
+ ```json5
+ {
+ 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:
+
+ ```nginx
+ 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";
+ }
+ ```
+
+
+
+ ```json5
+ {
+ gateway: {
+ bind: "lan",
+ trustedProxies: ["172.17.0.1"], // Traefik container IP
+ auth: {
+ mode: "trusted-proxy",
+ trustedProxy: {
+ userHeader: "x-forwarded-user",
+ },
+ },
+ },
+ }
+ ```
+
+
## Mixed token configuration
@@ -270,8 +297,7 @@ Loopback trusted-proxy auth also fails closed: same-host callers must supply the
## Operator scopes header
-Trusted-proxy auth is an **identity-bearing** HTTP mode, so callers may
-optionally declare operator scopes with `x-openclaw-scopes`.
+Trusted-proxy auth is an **identity-bearing** HTTP mode, so callers may optionally declare operator scopes with `x-openclaw-scopes`.
Examples:
@@ -287,26 +313,22 @@ Behavior:
- Gateway-auth **plugin HTTP routes** are narrower by default: when `x-openclaw-scopes` is absent, their runtime scope falls back to `operator.write`.
- 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:
+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.
-- 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.
-
-## Security Checklist
+## 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
-- [ ] **No loopback proxy source**: trusted-proxy auth fails closed for loopback-source requests
-- [ ] **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.token` and `gateway.auth.mode: "trusted-proxy"`
+- [ ] **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.
+- [ ] **No loopback proxy source**: trusted-proxy auth fails closed for loopback-source requests.
+- [ ] **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.token` and `gateway.auth.mode: "trusted-proxy"`.
-## Security Audit
+## Security audit
`openclaw security audit` will flag trusted-proxy auth with a **critical** severity finding. This is intentional — it's a reminder that you're delegating security to your proxy setup.
@@ -320,79 +342,95 @@ The audit checks for:
## Troubleshooting
-### "trusted_proxy_untrusted_source"
+
+
+ The request didn't come from an IP in `gateway.trustedProxies`. Check:
-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 inspect` or `kubectl get pods -o wide` to find actual IPs.
-- Is the proxy IP correct? (Docker container IPs can change)
-- Is there a load balancer in front of your proxy?
-- Use `docker inspect` or `kubectl get pods -o wide` to find actual IPs
+
+
+ OpenClaw rejected a loopback-source trusted-proxy request.
-### "trusted_proxy_loopback_source"
+ Check:
-OpenClaw rejected a loopback-source trusted-proxy request.
+ - 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?
-Check:
+ Fix:
-- 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?
+ - Use token/password auth for same-host loopback proxy setups, or
+ - Route through a non-loopback trusted proxy address and keep that IP in `gateway.trustedProxies`.
-Fix:
+
+
+ The user header was empty or missing. Check:
-- Use token/password auth for same-host loopback proxy setups, or
-- Route through a non-loopback trusted proxy address and keep that IP in `gateway.trustedProxies`.
+ - 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_user_missing"
+
+
+ A required header wasn't present. Check:
-The user header was empty or missing. Check:
+ - Your proxy configuration for those specific headers.
+ - Whether headers are being stripped somewhere in the chain.
-- 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?
+
+
+ The user is authenticated but not in `allowUsers`. Either add them or remove the allowlist.
+
+
+ Trusted-proxy auth succeeded, but the browser `Origin` header did not pass Control UI origin checks.
-### "trusted*proxy_missing_header*\*"
+ Check:
-A required header wasn't present. Check:
+ - `gateway.controlUi.allowedOrigins` includes 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=true` is set deliberately.
-- Your proxy configuration for those specific headers
-- Whether headers are being stripped somewhere in the chain
+
+
+ Make sure your proxy:
-### "trusted_proxy_user_not_allowed"
+ - 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.
-The user is authenticated but not in `allowUsers`. Either add them or remove the allowlist.
+
+
-### "trusted_proxy_origin_not_allowed"
-
-Trusted-proxy auth succeeded, but the browser `Origin` header did not pass Control UI origin checks.
-
-Check:
-
-- `gateway.controlUi.allowedOrigins` includes 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=true` is set deliberately
-
-### 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
+## Migration from token auth
If you're moving from token auth to trusted-proxy:
-1. Configure your proxy to authenticate users and pass headers
-2. Test the proxy setup independently (curl with headers)
-3. Update OpenClaw config with trusted-proxy auth
-4. Restart the Gateway
-5. Test WebSocket connections from the Control UI
-6. Run `openclaw security audit` and review findings
+
+
+ Configure your proxy to authenticate users and pass headers.
+
+
+ Test the proxy setup independently (curl with headers).
+
+
+ Update OpenClaw config with trusted-proxy auth.
+
+
+ Restart the Gateway.
+
+
+ Test WebSocket connections from the Control UI.
+
+
+ Run `openclaw security audit` and review findings.
+
+
## Related
-- [Security](/gateway/security) — full security guide
- [Configuration](/gateway/configuration) — config reference
-- [Remote Access](/gateway/remote) — other remote access patterns
+- [Remote access](/gateway/remote) — other remote access patterns
+- [Security](/gateway/security) — full security guide
- [Tailscale](/gateway/tailscale) — simpler alternative for tailnet-only access