Trusted proxy auth
When to use
Use trusted-proxy auth mode when:
- You run RemoteClaw 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 unauthorizederrors because browsers can’t pass tokens in WS payloads.
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).
How it works
Control UI pairing behavior
When gateway.auth.mode = "trusted-proxy" is active, device pairing is still
required for Control UI WebSocket sessions that present a device identity.
Passing trusted-proxy checks does not skip the pairing gate.
The two are answering different questions, and only one of them is delegated to your proxy:
- Trusted-proxy auth establishes who the user is.
- Device pairing establishes which device is speaking for that user.
Implications:
- Your reverse proxy auth policy and
allowUsersgate who may reach the Gateway; pairing remains the gate on what device may act. - Keep gateway ingress locked to trusted proxy IPs only (
gateway.trustedProxies+ firewall). - A trusted-proxy Control UI session is not issued a device token. Device tokens are bearer credentials that would outlive the proxy-fronted session and would authorize a direct connection that never traverses the proxy.
Two cases behave differently, and the difference is deliberate:
A device identity that is not paired is rejected. The connect fails with
pairing required (detail code PAIRING_REQUIRED) and a pairing request is
raised for approval, exactly as on any other transport. Previously such a device
was admitted on the strength of proxy auth alone and received the scopes it had
declared for itself, without ever having been paired.
Scope clearing without device identity: Because the browser over plain HTTP
cannot create the device identity that RemoteClaw uses to bind operator scopes,
trusted-proxy WebSocket connections that lack device identity entirely still
connect, but have their self-declared scopes cleared to an empty set. Scope-gated
methods (operator.read, operator.write, etc.) then fail with missing scope.
To preserve operator scopes on trusted-proxy WebSocket connections without
device identity — and to opt out of the pairing requirement above — set
gateway.controlUi.dangerouslyDisableDeviceAuth: true.
This is a break-glass flag (remoteclaw security audit reports it as critical).
Use it only when the reverse proxy is the sole path to the Gateway and device
identity cannot be established.
Configuration
{ gateway: { // Trusted-proxy auth requires a proxy source address the Gateway does not itself hold bind: "lan",
// CRITICAL: Only add your proxy's IP(s) here — the address the proxy connects FROM, // which is not the Gateway's own bridge/LAN address. 172.17.0.1 is correct when the // Gateway runs in a container and the proxy sits on the Docker host. trustedProxies: ["10.0.0.1", "172.17.0.1"],
auth: { mode: "trusted-proxy", 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"], }, }, },}- The proxy must connect from an address the Gateway does not itself hold. Trusted-proxy auth rejects the connecting (peer) address when it is loopback (
127.0.0.1,::1, loopback CIDRs), or when it matches any interface address the Gateway can enumerate — so the Gateway host cannot impersonate an upstream proxy. It also fails closed if those interfaces cannot be enumerated at all. There is no opt-in override for any of this. - This rules out a proxy that shares the Gateway’s network namespace — an ordinary same-host process reaching the Gateway over loopback or over one of its own interface addresses, or a container started with
--network=host. No configuration makes those work; usegateway.auth.passwordfor those callers. A proxy with its own network namespace is fine even on the same machine: a separate host, or a container/VM reaching the Gateway across a bridge, connects from an address the Gateway does not hold. List that source address ingateway.trustedProxies. - Internal Gateway clients that do not travel through the reverse proxy should use
gateway.auth.password/REMOTECLAW_GATEWAY_PASSWORD, not trusted-proxy identity headers. - Non-loopback Control UI deployments still need explicit
gateway.controlUi.allowedOrigins. - Forwarded-header evidence overrides loopback locality for local direct fallback. If a request arrives on loopback but carries
Forwarded, anyX-Forwarded-*, orX-Real-IPheader evidence, that evidence disqualifies local-direct password fallback and device-identity gating. Such a request is not rescued by trusted-proxy auth either, since its loopback source is rejected outright.
Configuration reference
TLS termination and HSTS
Use one TLS termination point and apply HSTS there.
- Good fit for internet-facing deployments.- Keeps certificate + HTTP hardening policy in one place.- RemoteClaw can stay on loopback HTTP behind the proxy.
Example header value:
```textStrict-Transport-Security: 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.Rollout guidance
- Start with a short max age first (for example
max-age=300) while validating traffic. - Increase to long-lived values (for example
max-age=31536000) only after confidence is high. - Add
includeSubDomainsonly if every subdomain is HTTPS-ready. - 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
```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:
```yamlroutes: - from: https://remoteclaw.example.com to: http://remoteclaw-gateway:18789 policy: - allow: or: - email: is: nick@example.com pass_identity_headers: true``````json5{ gateway: { bind: "lan", trustedProxies: ["10.0.0.1"], // Caddy/sidecar proxy IP auth: { mode: "trusted-proxy", trustedProxy: { userHeader: "x-forwarded-user", }, }, },}```
Caddyfile snippet:
```remoteclaw.example.com { authenticate with oauth2_provider authorize with policy1
reverse_proxy remoteclaw:18789 { header_up X-Forwarded-User {http.auth.user.email} }}``````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:
```nginxlocation / { auth_request /oauth2/auth; auth_request_set $user $upstream_http_x_auth_request_email;
proxy_pass http://remoteclaw: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";}```Mixed token configuration
RemoteClaw rejects ambiguous configurations where both a gateway.auth.token (or REMOTECLAW_GATEWAY_TOKEN) and trusted-proxy mode are active at the same time. Mixed token configs can cause loopback requests to silently authenticate on the wrong auth path.
If you see a mixed_trusted_proxy_token error on startup:
- Remove the shared token when using trusted-proxy mode, or
- Switch
gateway.auth.modeto"token"if you intend token-based auth.
Loopback trusted-proxy identity headers still fail closed: same-host callers are not silently authenticated as proxy users. Internal RemoteClaw callers that bypass the proxy may authenticate with gateway.auth.password / REMOTECLAW_GATEWAY_PASSWORD instead. Token fallback remains intentionally unsupported in trusted-proxy mode.
Operator scopes header
Trusted-proxy auth is an identity-bearing HTTP mode, so callers may optionally declare operator scopes with x-remoteclaw-scopes.
Note: x-remoteclaw-scopes applies to HTTP endpoints only. WebSocket scopes are
determined by the Gateway protocol handshake and device identity binding. For
WebSocket scope behavior with trusted-proxy, see
Control UI pairing behavior.
Examples:
x-remoteclaw-scopes: operator.readx-remoteclaw-scopes: operator.read,operator.writex-remoteclaw-scopes: operator.admin,operator.write
Behavior:
- When the header is present, RemoteClaw 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.
- Gateway-auth plugin HTTP routes are narrower by default: when
x-remoteclaw-scopesis absent, their runtime scope falls back tooperator.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: send x-remoteclaw-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
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.
- Proxy source is an address the Gateway does not hold: trusted-proxy auth fails closed for loopback and own-interface peer addresses, with no override. A proxy sharing the Gateway’s network namespace cannot be made to work; use
gateway.auth.passwordfor those callers. - 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.tokenandgateway.auth.mode: "trusted-proxy". - Local password fallback is private: If you configure
gateway.auth.passwordfor internal direct callers, keep the Gateway port firewalled so non-proxy remote clients cannot reach it directly.
Security audit
remoteclaw 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.
The audit checks for:
- Base
gateway.trusted_proxy_authwarning/critical reminder - Missing
trustedProxiesconfiguration - Missing
userHeaderconfiguration - Empty
allowUsers(allows any authenticated user) - Wildcard or missing browser-origin policy on exposed Control UI surfaces
Troubleshooting
- 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.Check:
- 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?
Fix:
- Prefer token/password auth for internal same-host clients that do not go through the proxy, or- Move the proxy so it connects from an address the Gateway does not hold, and keep that IP in `gateway.trustedProxies`.
There is no opt-in. A reverse proxy that reaches the Gateway over loopback cannot satisfy trusted-proxy auth in any configuration — the rejection is unconditional, so this is a deployment change, not a setting to flip.
Do not simply retarget the proxy at the host's own LAN address: that trades this error for `trusted_proxy_local_interface_source` below.This is what you get when a same-host proxy is pointed at the host's LAN or bridge address instead of `127.0.0.1`. It is the same restriction as `trusted_proxy_loopback_source`, not a separate one to work around.
Fix:
- Run the proxy where it has its own network namespace — a separate host, or a container/VM reaching the Gateway across a bridge — so its peer address is one the Gateway does not hold, and list that address in `gateway.trustedProxies`, or- Use `gateway.auth.password` for callers that must stay in the Gateway's own namespace.
A related reason code, `trusted_proxy_local_interface_check_failed`, means the Gateway could not enumerate its interfaces at all and failed closed.- 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?- Your proxy configuration for those specific headers.- Whether headers are being stripped somewhere in the chain.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.This is expected for trusted-proxy WebSocket connections without deviceidentity. Connections lacking device identity have their scopes cleared. Thebrowser cannot generate device identity over plain HTTP.
Fix:
- Set `gateway.controlUi.dangerouslyDisableDeviceAuth: true` to preserve operator scopes on trusted-proxy WebSocket connections, or- Use device identity pairing so scopes are bound to the device token.- 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
If you’re moving from token auth to trusted-proxy:
Related
- Configuration — config reference
- Remote access — other remote access patterns
- Security — full security guide
- Tailscale — simpler alternative for tailnet-only access