You put your app behind a tunnel (or any reverse proxy) to test
webhooks. Everything works. Then you notice something odd in your logs:
every single request comes from the same IP address.
Congratulations, you've met the X-Forwarded-For problem.
What actually happens
When a request flows through a tunnel, the TCP connection to your app
comes from the relay, not the real client. So request.remote_addr —
the value your framework uses for rate limiting, IP logging,
geo-blocking, brute-force detection — is the relay's address. For every
request. From every user.
The consequences are quiet and nasty:
Your rate limiter now rate-limits the relay, not the client. One
aggressive user trips the limit and everyone gets blocked. Or worse, the
limit is per-IP and effectively unlimited, because each relay node looks
like one "user."
* Your access logs are fiction. Security review of an incident?
Every entry says the same address.
* IP allowlists silently break. "Only allow my office IP" now
allows nothing, or everything, depending on how it's wired.
The fix (and its trap)
The proxy already tells you the real client IP — in the
X-Forwarded-For header. Every framework has a setting to trust it.
Flask: ProxyFix. Express: app.set('trust proxy', ...). Rails,
Django, Laravel: equivalents exist.
Here's the trap: trust that header blindly and anyone can spoof
it. A client can send X-Forwarded-For: 1.2.3.4 directly, and if your
app believes headers from anyone, your rate limiter is bypassed with a
curl flag.
The correct setup has two halves:
1. Trust `X-Forwarded-For` only when the immediate connection
comes from a proxy you control (your tunnel relay, your load balancer).
2. Strip or ignore the header on direct connections.
Most frameworks express this as "trusted proxies" — a list of proxy
IPs whose forwarded headers you believe. Set it. It's five minutes of
config that determines whether your security features are real or
decorative.
Why this matters more in the tunnel era
Tunnels used to be a demo-day tool. Now they're how teams test
webhooks, preview for clients, and let AI agents reach local
environments — often for days at a time, fronting apps with real auth
and real rate limits. Every one of those apps is behind a proxy, whether
the developer thought about it or not.
If you're picking a tunnel, this is one of the details that separates
serious ones: does the relay set and document its forwarded headers? We
built 21tunnel to pass real client IPs through
correctly (and gate tunnels at the edge when you don't want the internet
reaching your app at all) — but whatever you use, spend the five minutes
on trusted proxies.
Your rate limiter will thank you.
Top comments (0)