Forward and Reverse Proxies
Topic 55

Forward and Reverse Proxies

Proxies

A proxy is a machine that sits between a client and a server and relays traffic on behalf of one of them. The direction it faces decides everything. A forward proxy fronts the clients — it is the box your laptop's traffic goes through on the way out, used for egress filtering, caching, and hiding who the real client is. A reverse proxy fronts the servers — it is the box the public talks to, and it hides how many backends sit behind it and which one served the request.

The reverse proxy is the single most important component in a modern web stack. Load balancers, API gateways, and Kubernetes ingress controllers are all reverse proxies with extra features bolted on. nginx, Envoy, and HAProxy are the same tool wearing different configuration syntax. Once you understand what a reverse proxy reads, rewrites, and decides, the rest of this chapter is detail — every box from here on is a reverse proxy that specializes in one of those jobs.

Forward proxyfronts the clients
Sits in front of the clients, faces outward. The client is configured to use it; servers see the proxy's IP, not the workstations. Run by the client's org for egress filtering, caching, and anonymity.
Reverse proxyfronts the servers
Sits in front of the servers, the public front door. Owns the public IP and TLS cert on port 443; clients never know backends exist. Run by the service owner for TLS, routing, and load balancing.

Forward Proxy — the Client's Front Door

A forward proxy is configured on the client side and represents the client to the outside world. A corporate network routes all outbound HTTP through one so it can enforce egress policy: block known-bad domains, log every destination, cache frequently fetched objects, and present a single source IP to the internet. The servers being reached see the proxy's address, not the individual workstations behind it — which is exactly the anonymity property a forward proxy provides.

The defining trait is that the client knows the proxy exists and is configured to use it. Your browser has a proxy setting; the proxy does not magically intercept you unless someone deployed a transparent one inline. This is the inverse of a reverse proxy, where the client has no idea a proxy is involved at all and believes it is talking directly to the service.

Reverse Proxy — the Server's Front Door

A reverse proxy accepts connections from the public internet and forwards them to one of several backend servers on a private network. To the client it is the service: it owns the public IP, holds the TLS certificate for www.example.com, and answers on port 443. The actual application servers sit on 10.0.0.0/8 with no public address, reachable only through the proxy. This is the standard shape of every production web deployment.

A minimal nginx reverse proxy is a handful of lines: an upstream pool of backends and a server block that passes requests to it. The proxy opens its own TCP connection to a backend — the client's connection ends at the proxy, and a second connection carries the request the rest of the way.

# nginx: the public face for a pool of three app servers
upstream app {
    server 10.0.0.11:8080;
    server 10.0.0.12:8080;
    server 10.0.0.13:8080;
}
server {
    listen 443 ssl;
    server_name www.example.com;
    location / {
        proxy_pass http://app;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Headers a Reverse Proxy Rewrites

Because the backend's TCP connection comes from the proxy and not the client, the backend would see the proxy's IP as the source of every request — useless for logging, rate-limiting, or geolocation. The fix is the X-Forwarded-For header. The proxy records the real client IP there, and each proxy in a chain appends its view of the previous hop, building a comma-separated list left-to-right from original client to last proxy.

The application reads the leftmost entry as the true client — but only if it trusts the chain. Two companion headers travel with it: X-Forwarded-Proto tells the backend the original request was HTTPS even though the proxy-to-backend hop is plaintext, and the Host header must be preserved so name-based virtual hosting on the backend still resolves the right site. Drop or mangle Host and a backend serving many domains routes the request to the wrong one.

# two proxies in the path: client -> edge -> internal -> app
# the backend sees this header, left = original client
X-Forwarded-For: 203.0.113.7, 198.51.100.4
# 203.0.113.7   = real client
# 198.51.100.4  = edge proxy, as seen by the internal proxy

Termination versus Pass-Through

A reverse proxy can handle the connection in one of two modes. In termination mode it accepts the client's TLS connection, decrypts it, reads the full HTTP request, and opens a fresh connection to the backend — it sees and can act on the request content. This is what lets it route by path, rewrite headers, buffer slow uploads, and offload TLS crypto from the backends. The cost is that the proxy is now a full participant: it does the decryption work and the traffic behind it is plaintext unless you re-encrypt.

In pass-through mode the proxy forwards the encrypted bytes without decrypting, acting at the connection level only. It cannot read paths or headers — the payload is opaque ciphertext — so it cannot do content-based routing, but it preserves end-to-end encryption and adds almost no latency. The choice between them is the same tradeoff that runs through this whole chapter: read the content and gain control, or stay blind and stay fast. One subtlety of termination is buffering — a proxy that buffers the entire request body before contacting the backend protects slow backends from slow clients, but adds latency to large uploads if you forget to tune it.

Forward Proxy vs Reverse Proxy

Forward proxy sits in front of the clients and hides them from the servers. It is configured and operated by the client's organization — a corporate egress gateway, a caching proxy. Choose it when you need to control, filter, or anonymize traffic leaving a network.

Reverse proxy sits in front of the servers and hides them from the clients. It is configured and operated by the service owner — nginx, Envoy, a load balancer, an ingress controller. Choose it as the public front door for any service that needs TLS termination, routing, or load balancing — which is nearly all of them.

Common Mistakes
  • Trusting the client IP from the TCP connection when sitting behind a proxy. The backend sees the proxy's address on every request, so rate-limiting and geolocation key on one IP — you must read the leftmost X-Forwarded-For entry instead.
  • Trusting X-Forwarded-For from an untrusted source. A client can forge the header, so an app that blindly believes it lets attackers spoof any source IP; only honor it from proxies you control and strip it at the edge.
  • Running the reverse proxy as an unmonitored single point of failure. Every request funnels through it, so when it dies the whole service dies — yet teams routinely deploy one instance with no health check or failover.
  • Forwarding the wrong Host header. Set it to the upstream name instead of $host and a backend doing name-based virtual hosting serves the wrong site or a default 404, because it routes on the Host it receives.
  • Leaving request buffering at defaults for large uploads. A proxy that buffers the entire body before forwarding adds latency and disk I/O on multi-gigabyte uploads, turning a streaming upload into a stall.
Best Practices
  • Set X-Forwarded-For, X-Forwarded-Proto, and Host on every reverse proxy with proxy_set_header, so the backend can recover the real client IP, original scheme, and requested hostname.
  • Strip inbound X-Forwarded-For at the trust boundary and re-add it from the real connection, so an attacker cannot pre-seed a spoofed client IP that your app then trusts.
  • Run at least two reverse proxy instances behind a health-checked virtual IP, so the single point of failure becomes a pair and one node can die without taking the service down.
  • Configure an explicit list of trusted proxy IPs in the application (the trusted_proxies setting in most frameworks), so only those addresses are believed when parsing the forwarded chain.
  • Tune proxy_request_buffering off for streaming or large uploads, so the proxy forwards the body as it arrives instead of staging the whole payload first.
Comparable conceptsAPI gateway / ingress (specialized reverse proxies)NAT (the layer-3 analog)

Knowledge Check

A backend behind a reverse proxy logs the same source IP for every request. Why, and what recovers the real client IP?

  • The backend sees the proxy's connection; the real IP rides in X-Forwarded-For
  • The proxy rewrites every packet with NAT, so the original client IP is unrecoverable once forwarded
  • TLS encryption strips the client IP, so it only reappears if you turn encryption off entirely
  • The client IP is sitting in the Host header and the app is reading the wrong field for it

What distinguishes a forward proxy from a reverse proxy?

  • Which side it fronts and who runs it: forward fronts clients, reverse fronts servers
  • A forward proxy caches content for clients, whereas a reverse proxy never caches anything at all
  • A reverse proxy always speaks TLS while a forward proxy is always plaintext on every hop
  • A forward proxy operates at layer 4 and a reverse proxy operates up at layer 7

A reverse proxy in pass-through mode cannot route requests by URL path. Why not?

  • It forwards encrypted bytes undecrypted, so the path is ciphertext it can't read
  • It is simply too slow to parse the URL path before the request hits its timeout
  • The path sits in a request header that it deliberately strips out for privacy reasons
  • It supports only HTTP/1.1, a version that has no concept of a URL path at all

You got correct