Skip to content

Reverse Proxy

The previous article described a proxy the client sets up on purpose, forwarding requests to a destination the client already knows. A reverse proxy inverts that relationship entirely: the client thinks it's talking directly to the final destination, and has no idea — and generally no way to find out — that a completely different server actually handled the request.

The server the client never sees

When you visit a website, your browser connects to whatever address that domain resolves to and sends its request there, believing it's reached the application itself. Very often it hasn't. What it actually reached is a reverse proxy, which terminates that connection, decides which of possibly many backend servers should actually handle the request, opens its own separate connection to that backend, and relays the response back — all invisibly, from the client's point of view.

In a forward proxy, the client knows the destination and the destination doesn't know the client. In a reverse proxy, it's exactly the opposite: the client doesn't know the real destination, and the real destination is the one that knows everything. Load balancer, which this module already covered, is one specific and extremely common case of exactly this pattern — a reverse proxy that also decides which of several backends should handle each request. But a reverse proxy doesn't have to balance load across identical servers to earn the name; forwarding every request to one single backend, invisibly, is still a reverse proxy.

Client ──thinks this is the final destination──▶ Reverse proxy ──▶ Backend (client never sees this hop)

Why you'd put one in front of your own servers

  • TLS termination. Decrypting HTTPS once, at the reverse proxy, and running plain HTTP between the proxy and backends on a private network simplifies certificate management enormously — one place to renew and configure TLS, rather than every backend instance needing its own.
  • A single, stable entry point. Backends can be added, removed, or replaced without the client ever needing to know, because the client's only relationship is with the reverse proxy's address — this is what makes rolling deployments and horizontal scaling possible without touching DNS on every change.
  • Hiding internal topology. How many backend servers exist, what internal addresses they use, and how they're organized are all details a reverse proxy keeps entirely off the client's radar — genuinely useful for security, since an attacker probing the public-facing address learns nothing about what's actually behind it.
  • Serving multiple applications from one address. A reverse proxy can route /api to one backend and / to an entirely different one serving static files, letting one public IP and domain front services that have nothing to do with each other internally.

The header that carries the truth the connection itself hides

Once a reverse proxy terminates the client's connection and opens its own to the backend, the backend's view of "who connected to me" is just the reverse proxy's own address — the original client's IP is gone from the connection itself, replaced entirely by the proxy's. For most purposes that's exactly the intended behavior. But plenty of backend logic genuinely needs the real client address: rate limiting per user, geographic content decisions, fraud detection, access logs that mean anything.

The fix is a header, not a lower-layer mechanism: the reverse proxy adds an X-Forwarded-For header to the request before forwarding it, carrying the original client's IP address as plain request metadata. A backend that wants the real client address reads this header instead of the connection's source IP — but only if it's configured to trust it, which matters more than it sounds like it should, as the scenario below shows.

Practical scenario: rate limiting that punishes everyone at once

A team deploys a new API rate limiter directly on their backend application, configured to reject a client after 100 requests per minute, and the backend correctly reads each connection's source IP to identify who's making the requests. Within an hour of rollout, legitimate traffic starts getting rejected in bursts, seemingly at random, and the team's dashboards show one single "client" blowing past the rate limit constantly — even though no individual user is sending anywhere near that much traffic.

The application sits behind a reverse proxy, and the rate limiter was written to read the TCP connection's source IP directly rather than the X-Forwarded-For header. Every request the backend sees, regardless of which real client originated it, arrives with the reverse proxy's own IP address as its source — because that's genuinely accurate at the TCP layer; the proxy really did open that connection. From the backend's point of view, thousands of different users are indistinguishable from one enormous client, and the 100-requests-per-minute budget the whole system is supposed to share gets exhausted by combined traffic almost immediately, at which point everyone behind the proxy starts getting rejected together.

The fix is reading X-Forwarded-For instead of the raw connection source — but that fix has its own trap worth naming explicitly: a backend that blindly trusts an X-Forwarded-For header from any source is trivially spoofable, since nothing stops a malicious client from setting that header itself before the request even reaches the proxy. The header is only trustworthy when the backend is configured to only accept it from a known, controlled reverse proxy, and to strip or ignore any X-Forwarded-For value that arrives directly from an untrusted client rather than being appended by the proxy itself.

Warning

Never trust X-Forwarded-For from a backend that's directly reachable by clients without passing through your reverse proxy. If a client can reach the backend at all without going through the proxy, it can forge any IP address it wants in that header, and any access control built on top of it is worthless.

Practice exercises

  1. Explain the exact difference between a forward proxy and a reverse proxy in terms of which side — client or server — knows the true final destination.
  2. A backend's access logs show every single visitor coming from the same IP address. Diagnose what's most likely happening and name the header that would fix it.
  3. Explain why a backend should never trust an X-Forwarded-For value on a request that reached it directly, bypassing the reverse proxy — and what configuration change (on the network, not just the application) would close that gap.

This module has now covered every device that decides where traffic goes and whether it's allowed through at all. What's left is the piece of hardware doing the actual sending and receiving on behalf of every device this module has discussed — the interface that turns bits into a signal and back, on the one machine none of these devices exist without.

Sources