docs(trusted-proxy-auth): rewrite with Steps for handshake, Tabs for TLS, AccordionGroup for proxy examples and troubleshooting

This commit is contained in:
Vincent Koc
2026-04-26 01:04:51 -07:00
parent f2744978a0
commit 64a7a34c83

View File

@@ -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.
<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.
</Warning>
## 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
<Steps>
<Step title="Proxy authenticates the user">
Your reverse proxy authenticates users (OAuth, OIDC, SAML, etc.).
</Step>
<Step title="Proxy adds an identity header">
Proxy adds a header with the authenticated user identity (e.g., `x-forwarded-user: nick@example.com`).
</Step>
<Step title="Gateway verifies trusted source">
OpenClaw checks that the request came from a **trusted proxy IP** (configured in `gateway.trustedProxies`).
</Step>
<Step title="Gateway extracts identity">
OpenClaw extracts the user identity from the configured header.
</Step>
<Step title="Authorize">
If everything checks out, the request is authorized.
</Step>
</Steps>
## 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:
<Warning>
**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.
</Warning>
### 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. |
<ParamField path="gateway.trustedProxies" type="string[]" required>
Array of proxy IP addresses to trust. Requests from other IPs are rejected.
</ParamField>
<ParamField path="gateway.auth.mode" type="string" required>
Must be `"trusted-proxy"`.
</ParamField>
<ParamField path="gateway.auth.trustedProxy.userHeader" type="string" required>
Header name containing the authenticated user identity.
</ParamField>
<ParamField path="gateway.auth.trustedProxy.requiredHeaders" type="string[]">
Additional headers that must be present for the request to be trusted.
</ParamField>
<ParamField path="gateway.auth.trustedProxy.allowUsers" type="string[]">
Allowlist of user identities. Empty means allow all authenticated users.
</ParamField>
## TLS termination and HSTS
Use one TLS termination point and apply HSTS there.
### Recommended pattern: proxy TLS termination
<Tabs>
<Tab title="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.
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
```
</Tab>
<Tab title="Gateway TLS termination">
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.
</Tab>
</Tabs>
### 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
<AccordionGroup>
<Accordion title="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",
</Accordion>
<Accordion title="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",
},
},
},
},
},
}
```
}
```
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
</Accordion>
<Accordion title="nginx + oauth2-proxy">
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";
}
```
</Accordion>
<Accordion title="Traefik with forward auth">
```json5
{
gateway: {
bind: "lan",
trustedProxies: ["172.17.0.1"], // Traefik container IP
auth: {
mode: "trusted-proxy",
trustedProxy: {
userHeader: "x-forwarded-user",
},
},
},
}
```
</Accordion>
</AccordionGroup>
## 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"
<AccordionGroup>
<Accordion title="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
</Accordion>
<Accordion title="trusted_proxy_loopback_source">
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:
</Accordion>
<Accordion title="trusted_proxy_user_missing">
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"
</Accordion>
<Accordion title="trusted_proxy_missing_header_*">
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?
</Accordion>
<Accordion title="trusted_proxy_user_not_allowed">
The user is authenticated but not in `allowUsers`. Either add them or remove the allowlist.
</Accordion>
<Accordion title="trusted_proxy_origin_not_allowed">
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
</Accordion>
<Accordion title="WebSocket still failing">
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.
</Accordion>
</AccordionGroup>
### "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
<Steps>
<Step title="Configure the proxy">
Configure your proxy to authenticate users and pass headers.
</Step>
<Step title="Test the proxy independently">
Test the proxy setup independently (curl with headers).
</Step>
<Step title="Update OpenClaw config">
Update OpenClaw config with trusted-proxy auth.
</Step>
<Step title="Restart the Gateway">
Restart the Gateway.
</Step>
<Step title="Test WebSocket">
Test WebSocket connections from the Control UI.
</Step>
<Step title="Audit">
Run `openclaw security audit` and review findings.
</Step>
</Steps>
## 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