You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(security): resolve the client IP from the trusted proxy chain, not the leftmost header
The leftmost x-forwarded-for token is written by the client, so every
IP-derived decision was steerable by sending your own header: per-IP rate
limits could be reset per request, audit rows recorded an attacker-chosen
address, and per-webhook IP allowlists could be satisfied outright.
Resolution now walks the chain right to left against AUTH_TRUSTED_PROXIES
and returns null when no address can be trusted, delegating to Better
Auth's own resolver so this agrees by construction with the ipAddress on
every session row. Callers split on that null deliberately: rate limits
share one strict bucket, the webhook allowlist denies, audit records blank.
Copy file name to clipboardExpand all lines: apps/docs/content/docs/en/platform/self-hosting/authentication.mdx
+10-2Lines changed: 10 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -109,13 +109,21 @@ Both SSO flags are needed: the server-side one grants access, and the `NEXT_PUBL
109
109
110
110
## Behind a load balancer
111
111
112
-
Tell Better Auth which forwarding hops to trust when resolving the client IP:
112
+
Tell Sim which forwarding hops to trust when resolving the client IP:
113
113
114
114
```bash
115
115
AUTH_TRUSTED_PROXIES=10.0.0.0/24,192.0.2.10
116
116
```
117
117
118
-
Better Auth walks `X-Forwarded-For` right to left, skips these hops, and uses the first untrusted address as the client IP for session records and its own IP-based checks. Use your proxies' actual addresses — a broad private range that also covers client traffic defeats the purpose. See [Security](/platform/self-hosting/security).
118
+
The forwarded chain is walked right to left, skipping these hops, and the first untrusted address becomes the client IP. Use your proxies' actual addresses — a broad private range that also covers client traffic defeats the purpose, and `0.0.0.0/0` matches every hop, so the walk finds no client at all.
119
+
120
+
This one variable governs every IP-derived behavior in Sim: session records, per-IP rate limits on public endpoints, the `ipAddress` column on audit log entries, and per-webhook IP allowlists.
121
+
122
+
<Callouttype="warn">
123
+
Leave it unset behind a proxy and Sim resolves no client IP at all. Nothing breaks outright — per-IP rate limits fall back to one shared bucket per endpoint (stricter, not weaker), audit rows record a blank IP, and any webhook configured with an IP allowlist rejects every delivery. Sim will not read the leftmost `X-Forwarded-For` value as a fallback, because a client can set that header itself.
124
+
</Callout>
125
+
126
+
Sim reads `X-Forwarded-For` first and falls back to `X-Real-IP`. If your ingress sets only `X-Real-IP` (nginx's `proxy_set_header X-Real-IP $remote_addr`), that is enough — but make sure the header is *overwritten* at the edge rather than passed through from the client. See [Security](/platform/self-hosting/security).
Copy file name to clipboardExpand all lines: apps/docs/content/docs/en/platform/self-hosting/environment-variables.mdx
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -30,7 +30,7 @@ import { Callout } from 'fumadocs-ui/components/callout'
30
30
|`REDIS_TLS_SERVERNAME`| TLS SNI override. Required when `REDIS_URL` uses `rediss://` with a bare IP, or the app throws at startup |
31
31
|`NEXT_PUBLIC_SOCKET_URL`| WebSocket URL — defaults to the page origin; set only if realtime is on a separate host |
32
32
|`TRUSTED_ORIGINS`| Comma-separated additional origins to trust for auth (apex + `www`, alias domains) |
33
-
|`AUTH_TRUSTED_PROXIES`| Comma-separated reverse-proxy IPs/CIDRs so the client IP cannot be forged through `X-Forwarded-For`|
33
+
|`AUTH_TRUSTED_PROXIES`| Comma-separated reverse-proxy IPs/CIDRs so the client IP cannot be forged through `X-Forwarded-For`. Governs sessions, per-IP rate limits, audit log IPs, and webhook IP allowlists — behind a proxy, leaving it unset means no client IP resolves at all|
34
34
|`INTERNAL_API_BASE_URL`| Internal URL for server-side self-calls, e.g. `http://sim-app.simstudio.svc.cluster.local:3000`. Required for PII log redaction; defaults to `NEXT_PUBLIC_APP_URL`|
35
35
|`DATABASE_REPLICA_URL`| Read-replica connection string for log listing, audit logs, and dashboard aggregations. Falls back to the primary when unset |
Behind a load balancer, `X-Forwarded-For` is client-controllable. Set `AUTH_TRUSTED_PROXIES` to your proxies' actual addresses so Better Auth resolves the real client IP, and `TRUSTED_ORIGINS` if users reach Sim from more than one origin. Both are covered in [Authentication](/platform/self-hosting/authentication#behind-a-load-balancer).
151
+
`X-Forwarded-For`is appended to by each hop, and the leftmost value is written by the client — so behind a load balancer it is attacker-controlled. Sim never reads it directly. It walks the chain right to left, skips the hops you declare in `AUTH_TRUSTED_PROXIES`, and takes the first address it does not recognize as a proxy. A chain it cannot verify resolves to no IP at all rather than to a guess.
152
+
153
+
Set `AUTH_TRUSTED_PROXIES` to your proxies' actual addresses, and `TRUSTED_ORIGINS` if users reach Sim from more than one origin. Both are covered in [Authentication](/platform/self-hosting/authentication#behind-a-load-balancer).
154
+
155
+
What depends on it:
156
+
157
+
| Surface | With it set | Without it, behind a proxy |
158
+
|---|---|---|
159
+
| Session records | Real client IP on the session row | Blank |
160
+
| Per-IP rate limits on public endpoints | One bucket per client | One shared bucket per endpoint — stricter, and not amplifiable |
161
+
| Audit log `ipAddress` | Real client IP | Blank |
162
+
| Per-webhook IP allowlists | Enforced against the real client | Every delivery rejected |
163
+
164
+
<Callout type="warn">
165
+
Do not set it to `0.0.0.0/0` (or `::/0`) to "just make it work". Every hop then counts as a trusted proxy, the walk runs past the end of the chain, and *no* request resolves an IP. Sim logs an error at startup if you do, and likewise if an entry is not a valid IP or CIDR — a typo silently drops that entry.
166
+
</Callout>
152
167
153
168
## Restricting who can use the instance
154
169
@@ -179,7 +194,7 @@ The service bundles ~2.2 GB of spaCy models, so first start takes around three m
179
194
- [ ] Images pinned to an explicit tag or digest on app, realtime, and migrations
180
195
- [ ] TLS terminating at the ingress; HTTP redirected or disabled
181
196
- [ ] `NEXT_PUBLIC_APP_URL` and `BETTER_AUTH_URL` set to the real public origin
182
-
- [ ] `AUTH_TRUSTED_PROXIES` set if behind a load balancer
197
+
- [ ] `AUTH_TRUSTED_PROXIES` set to the ingress addresses if behind a load balancer (not `0.0.0.0/0`)
183
198
- [ ] Signup restricted (`DISABLE_REGISTRATION` or `ALLOWED_LOGIN_DOMAINS`)
184
199
- [ ] `DISABLE_AUTH` **not** set
185
200
- [ ] NetworkPolicy enabled and `ingressFrom` scoped to the ingress controller
# INTERNAL_API_BASE_URL=http://sim-app.default.svc.cluster.local:3000 # Optional: internal URL for server-side /api self-calls; defaults to NEXT_PUBLIC_APP_URL
21
21
# TRUSTED_ORIGINS=https://www.example.com,https://app.example.com # Optional: comma-separated additional public origins to trust for auth (apex+www, alias domains). Merged into Better Auth trustedOrigins.
22
-
# AUTH_TRUSTED_PROXIES=10.0.0.0/24,192.0.2.10 # Optional: reverse-proxy IPs/CIDRs in front of the app. Better Auth walks x-forwarded-for right to left, skips these hops, and uses the first untrusted address as the client IP (prevents forwarded-header spoofing). Use your proxies' actual addresses, not broad private ranges that also cover clients.
22
+
# AUTH_TRUSTED_PROXIES=10.0.0.0/24,192.0.2.10 # Optional: reverse-proxy IPs/CIDRs in front of the app. Sim walks x-forwarded-for right to left, skips these hops, and uses the first untrusted address as the client IP (prevents forwarded-header spoofing). Governs session IPs, per-IP rate limits, audit log IPs, and webhook IP allowlists. Use your proxies' actual addresses — not broad private ranges that also cover clients, and never 0.0.0.0/0 (matches every hop, so nothing resolves).
23
23
24
24
# Chat (Optional)
25
25
# COPILOT_API_KEY= # Mint one at https://sim.ai. Without it the Sim Chat block, prompt jobs, and Inbox cannot run
0 commit comments