fix(http): trust X-Real-IP only behind a declared proxy

The client-IP resolver read X-Real-IP on every path: with
TRUST_PROXY_HOPS=0, which means no proxy in front and no forwarding header
to believe, and on the path where the X-Forwarded-For chain was shorter
than the declared hops, which means the proxies were bypassed. A caller
sending a new X-Real-IP with each request got a new per-IP rate-limit
bucket each time.

X-Real-IP now gets the trust X-Forwarded-For gets: it is read only when at
least one proxy hop is declared and the request carries no X-Forwarded-For.
A short chain resolves to no address. The reverse-proxy guide no longer
suggests TRUST_PROXY_HOPS=0 for a proxy that sets X-Real-IP; such a proxy
takes 1.
This commit is contained in:
Marc-André Bombeck committed 2026-09-26 20:10:12 +02:00
1 parent 92c5b0a60d
commit bc69dec8e6
4 files changed
+50 -26

No files matched your search

+14 -8
View File
@@ -40,17 +40,23 @@ chain from the **right**, counting back `TRUST_PROXY_HOPS` entries.
This guards against a client rotating `X-Forwarded-For` per request
to defeat IP-based rate limits.
| Topology | `TRUST_PROXY_HOPS` |
| --------------------------------------------------------- | -------------------------- |
| App is internet-facing with no proxy | `0` (XFF ignored entirely) |
| Single proxy in front (Caddy / Traefik / Nginx / Coolify) | `1` (default) |
| Cloudflare → your proxy → app | `2` |
| Cloudflare → Coolify-Tunnel → Coolify → app | `3` |
| Topology | `TRUST_PROXY_HOPS` |
| --------------------------------------------------------- | ------------------------------------- |
| App is internet-facing with no proxy | `0` (no forwarding header is trusted) |
| Single proxy in front (Caddy / Traefik / Nginx / Coolify) | `1` (default) |
| Cloudflare → your proxy → app | `2` |
| Cloudflare → Coolify-Tunnel → Coolify → app | `3` |
A misconfigured count logs a one-shot warning to stderr and collapses
every anonymous caller into one shared rate-limit bucket. Match the
value to the actual hop count or set it to `0` and let `x-real-ip`
drive the IP resolution.
value to the actual hop count.
`X-Real-IP` gets the same trust as `X-Forwarded-For`. It is read only
when `TRUST_PROXY_HOPS` is `1` or more and the request carries no
`X-Forwarded-For`, which covers a proxy that sets only `X-Real-IP`. With
`0` neither header is believed, because without a proxy in front both
come from the caller. If your proxy sets only `X-Real-IP` and you had
set `0` for it, set `1` instead.
## Caddy