{"id":613,"date":"2026-03-21T22:52:56","date_gmt":"2026-03-21T14:52:56","guid":{"rendered":"https:\/\/pa.yingzhi8.cn\/index.php\/2026\/03\/21\/gateway-trusted-proxy-auth\/"},"modified":"2026-03-21T23:28:51","modified_gmt":"2026-03-21T15:28:51","slug":"gateway-trusted-proxy-auth","status":"publish","type":"post","link":"https:\/\/pa.yingzhi8.cn\/index.php\/2026\/03\/21\/gateway-trusted-proxy-auth\/","title":{"rendered":"Trusted Proxy Auth"},"content":{"rendered":"<h1>Trusted Proxy Auth<\/h1>\n<blockquote>\n<p>\u26a0\ufe0f <strong>Security-sensitive feature.<\/strong> This mode delegates authentication entirely to your reverse proxy. Misconfiguration can expose your Gateway \u7f51\u5173 to unauthorized access. Read this page carefully before enabling.<\/p>\n<\/blockquote>\n<h2>When to Use<\/h2>\n<p>Use <code>trusted-proxy<\/code> auth mode when:<\/p>\n<ul>\n<li>You run OpenClaw behind an <strong>identity-aware proxy<\/strong> (Pomerium, Caddy + OAuth, nginx + oauth2-proxy, Traefik + forward auth)<\/li>\n<li>Your proxy handles all authentication and passes user identity via headers<\/li>\n<li>You&#8217;re in a Kubernetes or container environment where the proxy is the only path to the Gateway \u7f51\u5173<\/li>\n<li>You&#8217;re hitting WebSocket <code>1008 unauthorized<\/code> errors because browsers can&#8217;t pass tokens in WS payloads<\/li>\n<\/ul>\n<h2>When NOT to Use<\/h2>\n<ul>\n<li>If your proxy doesn&#8217;t authenticate users (just a TLS terminator or load balancer)<\/li>\n<li>If there&#8217;s any path to the Gateway \u7f51\u5173 that bypasses the proxy (firewall holes, internal network access)<\/li>\n<li>If you&#8217;re unsure whether your proxy correctly strips\/overwrites forwarded headers<\/li>\n<li>If you only need personal single-user access (consider Tailscale Serve + loopback for simpler setup)<\/li>\n<\/ul>\n<h2>How It Works<\/h2>\n<ol>\n<li>Your reverse proxy authenticates users (OAuth, OIDC, SAML, etc.)<\/li>\n<li>Proxy adds a header with the authenticated user identity (e.g., <code>x-forwarded-user: nick@example.com<\/code>)<\/li>\n<li>OpenClaw checks that the request came from a <strong>trusted proxy IP<\/strong> (configured in <code>gateway.trustedProxies<\/code>)<\/li>\n<li>OpenClaw extracts the user identity from the configured header<\/li>\n<li>If everything checks out, the request is authorized<\/li>\n<\/ol>\n<h2>Control UI Pairing Behavior<\/h2>\n<p>When <code>gateway.auth.mode = \"trusted-proxy\"<\/code> is active and the request passes<br \/>\ntrusted-proxy checks, Control UI WebSocket sessions can connect without device<br \/>\npairing identity.<\/p>\n<p>Implications:<\/p>\n<ul>\n<li>Pairing is no longer the primary gate for Control UI access in this mode.<\/li>\n<li>Your reverse proxy auth policy and <code>allowUsers<\/code> become the effective access control.<\/li>\n<li>Keep \u7f51\u5173 ingress locked to trusted proxy IPs only (<code>gateway.trustedProxies<\/code> + firewall).<\/li>\n<\/ul>\n<h2>\u914d\u7f6e\u8bf4\u660e<\/h2>\n<p>&#8220;`json5  theme={&#8220;theme&#8221;:{&#8220;light&#8221;:&#8221;min-light&#8221;,&#8221;dark&#8221;:&#8221;min-dark&#8221;}}<br \/>\n{<br \/>\n  \u7f51\u5173: {<br \/>\n    \/\/ Use loopback for same-host proxy setups; use lan\/custom for remote proxy hosts<br \/>\n    bind: &#8220;loopback&#8221;,<\/p>\n<pre><code>\/\/ CRITICAL: Only add your proxy's IP(s) here\ntrustedProxies: [\"10.0.0.1\", \"172.17.0.1\"],\n\nauth: {\n  mode: \"trusted-proxy\",\n  trustedProxy: {\n    \/\/ Header containing authenticated user identity (required)\n    userHeader: \"x-forwarded-user\",\n\n    \/\/ Optional: headers that MUST be present (proxy verification)\n    requiredHeaders: [\"x-forwarded-proto\", \"x-forwarded-host\"],\n\n    \/\/ Optional: restrict to specific users (empty = allow all)\n    allowUsers: [\"nick@example.com\", \"admin@company.org\"],\n  },\n},\n<\/code><\/pre>\n<p>},<br \/>\n}<\/p>\n<pre><code>\nIf `gateway.bind` is `loopback`, include a loopback proxy address in\n`gateway.trustedProxies` (`127.0.0.1`, `::1`, or an equivalent loopback CIDR).\n\n### Configuration Reference\n\n| Field                                       | Required | Description                                                                 |\n| ------------------------------------------- | -------- | --------------------------------------------------------------------------- |\n| `gateway.trustedProxies`                    | Yes      | Array of proxy IP addresses to trust. Requests from other IPs are rejected. |\n| `gateway.auth.mode`                         | Yes      | Must be `&quot;trusted-proxy&quot;`                                                   |\n| `gateway.auth.trustedProxy.userHeader`      | Yes      | Header name containing the authenticated user identity                      |\n| `gateway.auth.trustedProxy.requiredHeaders` | No       | Additional headers that must be present for the request to be trusted       |\n| `gateway.auth.trustedProxy.allowUsers`      | No       | Allowlist of user identities. Empty means allow all authenticated users.    |\n\n## TLS termination and HSTS\n\nUse one TLS termination point and apply HSTS there.\n\n### Recommended pattern: proxy TLS termination\n\nWhen your reverse proxy handles HTTPS for `https:\/\/control.example.com`, set\n`Strict-Transport-Security` at the proxy for that domain.\n\n* Good fit for internet-facing deployments.\n* Keeps certificate + HTTP hardening policy in one place.\n* OpenClaw can stay on loopback HTTP behind the proxy.\n\nExample header value:\n\n```text  theme={&quot;theme&quot;:{&quot;light&quot;:&quot;min-light&quot;,&quot;dark&quot;:&quot;min-dark&quot;}}\nStrict-Transport-Security: max-age=31536000; includeSubDomains\n<\/code><\/pre>\n<h3>Gateway \u7f51\u5173 TLS termination<\/h3>\n<p>If OpenClaw itself serves HTTPS directly (no TLS-terminating proxy), set:<\/p>\n<p>&#8220;`json5  theme={&#8220;theme&#8221;:{&#8220;light&#8221;:&#8221;min-light&#8221;,&#8221;dark&#8221;:&#8221;min-dark&#8221;}}<br \/>\n{<br \/>\n  \u7f51\u5173: {<br \/>\n    tls: { enabled: true },<br \/>\n    http: {<br \/>\n      securityHeaders: {<br \/>\n        strictTransportSecurity: &#8220;max-age=31536000; includeSubDomains&#8221;,<br \/>\n      },<br \/>\n    },<br \/>\n  },<br \/>\n}<\/p>\n<pre><code>\n`strictTransportSecurity` accepts a string header value, or `false` to disable explicitly.\n\n### Rollout guidance\n\n* Start with a short max age first (for example `max-age=300`) while validating traffic.\n* Increase to long-lived values (for example `max-age=31536000`) only after confidence is high.\n* Add `includeSubDomains` only if every subdomain is HTTPS-ready.\n* Use preload only if you intentionally meet preload requirements for your full domain set.\n* Loopback-only local development does not benefit from HSTS.\n\n## Proxy Setup Examples\n\n### Pomerium\n\nPomerium passes identity in `x-pomerium-claim-email` (or other claim headers) and a JWT in `x-pomerium-jwt-assertion`.\n\n```json5  theme={&quot;theme&quot;:{&quot;light&quot;:&quot;min-light&quot;,&quot;dark&quot;:&quot;min-dark&quot;}}\n{\n  gateway: {\n    bind: &quot;lan&quot;,\n    trustedProxies: [&quot;10.0.0.1&quot;], \/\/ Pomerium's IP\n    auth: {\n      mode: &quot;trusted-proxy&quot;,\n      trustedProxy: {\n        userHeader: &quot;x-pomerium-claim-email&quot;,\n        requiredHeaders: [&quot;x-pomerium-jwt-assertion&quot;],\n      },\n    },\n  },\n}\n<\/code><\/pre>\n<p>Pomerium config snippet:<\/p>\n<p>&#8220;`yaml  theme={&#8220;theme&#8221;:{&#8220;light&#8221;:&#8221;min-light&#8221;,&#8221;dark&#8221;:&#8221;min-dark&#8221;}}<br \/>\nroutes:<br \/>\n  &#8211; from: https:\/\/openclaw.example.com<br \/>\n    to: http:\/\/openclaw-\u7f51\u5173:18789<br \/>\n    policy:<br \/>\n      &#8211; allow:<br \/>\n          or:<br \/>\n            &#8211; email:<br \/>\n                is: nick@example.com<br \/>\n    pass_identity_headers: true<\/p>\n<pre><code>\n### Caddy with OAuth\n\nCaddy with the `caddy-security` plugin can authenticate users and pass identity headers.\n\n```json5  theme={&quot;theme&quot;:{&quot;light&quot;:&quot;min-light&quot;,&quot;dark&quot;:&quot;min-dark&quot;}}\n{\n  gateway: {\n    bind: &quot;lan&quot;,\n    trustedProxies: [&quot;127.0.0.1&quot;], \/\/ Caddy's IP (if on same host)\n    auth: {\n      mode: &quot;trusted-proxy&quot;,\n      trustedProxy: {\n        userHeader: &quot;x-forwarded-user&quot;,\n      },\n    },\n  },\n}\n<\/code><\/pre>\n<p>Caddyfile snippet:<\/p>\n<pre><code>openclaw.example.com {\n    authenticate with oauth2_provider\n    authorize with policy1\n\n    reverse_proxy openclaw:18789 {\n        header_up X-Forwarded-User {http.auth.user.email}\n    }\n}\n<\/code><\/pre>\n<h3>nginx + oauth2-proxy<\/h3>\n<p>oauth2-proxy authenticates users and passes identity in <code>x-auth-request-email<\/code>.<\/p>\n<p>&#8220;`json5  theme={&#8220;theme&#8221;:{&#8220;light&#8221;:&#8221;min-light&#8221;,&#8221;dark&#8221;:&#8221;min-dark&#8221;}}<br \/>\n{<br \/>\n  \u7f51\u5173: {<br \/>\n    bind: &#8220;lan&#8221;,<br \/>\n    trustedProxies: [&#8220;10.0.0.1&#8221;], \/\/ nginx\/oauth2-proxy IP<br \/>\n    auth: {<br \/>\n      mode: &#8220;trusted-proxy&#8221;,<br \/>\n      trustedProxy: {<br \/>\n        userHeader: &#8220;x-auth-request-email&#8221;,<br \/>\n      },<br \/>\n    },<br \/>\n  },<br \/>\n}<\/p>\n<pre><code>\nnginx config snippet:\n\n```nginx  theme={&quot;theme&quot;:{&quot;light&quot;:&quot;min-light&quot;,&quot;dark&quot;:&quot;min-dark&quot;}}\nlocation \/ {\n    auth_request \/oauth2\/auth;\n    auth_request_set $user $upstream_http_x_auth_request_email;\n\n    proxy_pass http:\/\/openclaw:18789;\n    proxy_set_header X-Auth-Request-Email $user;\n    proxy_http_version 1.1;\n    proxy_set_header Upgrade $http_upgrade;\n    proxy_set_header Connection &quot;upgrade&quot;;\n}\n<\/code><\/pre>\n<h3>Traefik with Forward Auth<\/h3>\n<p><code>json5  theme={\"theme\":{\"light\":\"min-light\",\"dark\":\"min-dark\"}}<br \/>\n{<br \/>\n  gateway: {<br \/>\n    bind: \"lan\",<br \/>\n    trustedProxies: [\"172.17.0.1\"], \/\/ Traefik container IP<br \/>\n    auth: {<br \/>\n      mode: \"trusted-proxy\",<br \/>\n      trustedProxy: {<br \/>\n        userHeader: \"x-forwarded-user\",<br \/>\n      },<br \/>\n    },<br \/>\n  },<br \/>\n}<\/code><\/p>\n<h2>Security Checklist<\/h2>\n<p>Before enabling trusted-proxy auth, verify:<\/p>\n<ul>\n<li>[ ] <strong>Proxy is the only path<\/strong>: The Gateway \u7f51\u5173 port is firewalled from everything except your proxy<\/li>\n<li>[ ] <strong>trustedProxies is minimal<\/strong>: Only your actual proxy IPs, not entire subnets<\/li>\n<li>[ ] <strong>Proxy strips headers<\/strong>: Your proxy overwrites (not appends) <code>x-forwarded-*<\/code> headers from clients<\/li>\n<li>[ ] <strong>TLS termination<\/strong>: Your proxy handles TLS; users connect via HTTPS<\/li>\n<li>[ ] <strong>allowUsers is set<\/strong> (recommended): Restrict to known users rather than allowing anyone authenticated<\/li>\n<\/ul>\n<h2>Security Audit<\/h2>\n<p><code>openclaw security audit<\/code> will flag trusted-proxy auth with a <strong>critical<\/strong> severity finding. This is intentional \u2014 it&#8217;s a reminder that you&#8217;re delegating security to your proxy setup.<\/p>\n<p>The audit checks for:<\/p>\n<ul>\n<li>Missing <code>trustedProxies<\/code> configuration<\/li>\n<li>Missing <code>userHeader<\/code> configuration<\/li>\n<li>Empty <code>allowUsers<\/code> (allows any authenticated user)<\/li>\n<\/ul>\n<h2>\u6545\u969c\u6392\u67e5<\/h2>\n<h3>&#8220;trusted_proxy_untrusted_source&#8221;<\/h3>\n<p>The request didn&#8217;t come from an IP in <code>gateway.trustedProxies<\/code>. Check:<\/p>\n<ul>\n<li>Is the proxy IP correct? (Docker container IPs can change)<\/li>\n<li>Is there a load balancer in front of your proxy?<\/li>\n<li>Use <code>docker inspect<\/code> or <code>kubectl get pods -o wide<\/code> to find actual IPs<\/li>\n<\/ul>\n<h3>&#8220;trusted_proxy_user_missing&#8221;<\/h3>\n<p>The user header was empty or missing. Check:<\/p>\n<ul>\n<li>Is your proxy configured to pass identity headers?<\/li>\n<li>Is the header name correct? (case-insensitive, but spelling matters)<\/li>\n<li>Is the user actually authenticated at the proxy?<\/li>\n<\/ul>\n<h3>&#8220;trusted<em>proxy_missing_header<\/em>*&#8221;<\/h3>\n<p>A required header wasn&#8217;t present. Check:<\/p>\n<ul>\n<li>Your proxy configuration for those specific headers<\/li>\n<li>Whether headers are being stripped somewhere in the chain<\/li>\n<\/ul>\n<h3>&#8220;trusted_proxy_user_not_allowed&#8221;<\/h3>\n<p>The user is authenticated but not in <code>allowUsers<\/code>. Either add them or remove the allowlist.<\/p>\n<h3>WebSocket Still Failing<\/h3>\n<p>Make sure your proxy:<\/p>\n<ul>\n<li>Supports WebSocket upgrades (<code>Upgrade: websocket<\/code>, <code>Connection: upgrade<\/code>)<\/li>\n<li>Passes the identity headers on WebSocket upgrade requests (not just HTTP)<\/li>\n<li>Doesn&#8217;t have a separate auth path for WebSocket connections<\/li>\n<\/ul>\n<h2>Migration from Token Auth<\/h2>\n<p>If you&#8217;re moving from token auth to trusted-proxy:<\/p>\n<ol>\n<li>Configure your proxy to authenticate users and pass headers<\/li>\n<li>Test the proxy setup independently (curl with headers)<\/li>\n<li>Update OpenClaw config with trusted-proxy auth<\/li>\n<li>Restart the Gateway \u7f51\u5173<\/li>\n<li>Test WebSocket connections from the Control UI<\/li>\n<li>Run <code>openclaw security audit<\/code> and review findings<\/li>\n<\/ol>\n<h2>Related<\/h2>\n<ul>\n<li><a href=\"https:\/\/pa.yingzhi8.cn\/?p=747\">Security<\/a> \u2014 full security guide<\/li>\n<li><a href=\"https:\/\/pa.yingzhi8.cn\/?p=120\">\u914d\u7f6e\u8bf4\u660e<\/a> \u2014 config reference<\/li>\n<li><a href=\"https:\/\/pa.yingzhi8.cn\/?p=137\">Remote Access<\/a> \u2014 other remote access patterns<\/li>\n<li><a href=\"https:\/\/pa.yingzhi8.cn\/?p=142\">Tailscale<\/a> \u2014 simpler alternative for tailnet-only access<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Trusted Proxy Auth \u26a0\ufe0f Security-sensitive feature. This  [&hellip;]<\/p>\n","protected":false},"author":0,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2],"tags":[],"class_list":["post-613","post","type-post","status-publish","format-standard","hentry","category-docs"],"_links":{"self":[{"href":"https:\/\/pa.yingzhi8.cn\/index.php\/wp-json\/wp\/v2\/posts\/613","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/pa.yingzhi8.cn\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/pa.yingzhi8.cn\/index.php\/wp-json\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/pa.yingzhi8.cn\/index.php\/wp-json\/wp\/v2\/comments?post=613"}],"version-history":[{"count":3,"href":"https:\/\/pa.yingzhi8.cn\/index.php\/wp-json\/wp\/v2\/posts\/613\/revisions"}],"predecessor-version":[{"id":814,"href":"https:\/\/pa.yingzhi8.cn\/index.php\/wp-json\/wp\/v2\/posts\/613\/revisions\/814"}],"wp:attachment":[{"href":"https:\/\/pa.yingzhi8.cn\/index.php\/wp-json\/wp\/v2\/media?parent=613"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/pa.yingzhi8.cn\/index.php\/wp-json\/wp\/v2\/categories?post=613"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/pa.yingzhi8.cn\/index.php\/wp-json\/wp\/v2\/tags?post=613"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}