From 64a7a34c83343e3add5638eb34f48693abaf05dc Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 26 Apr 2026 01:04:51 -0700 Subject: [PATCH] docs(trusted-proxy-auth): rewrite with Steps for handshake, Tabs for TLS, AccordionGroup for proxy examples and troubleshooting --- docs/gateway/trusted-proxy-auth.md | 494 ++++++++++++++++------------- 1 file changed, 266 insertions(+), 228 deletions(-) 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