Edgeproxyforward proxyreverse proxynginxenvoy

Forward and Reverse Proxies

A proxy is a process that terminates one connection and opens another; whether it acts for the client (forward) or for the server fleet (reverse) decides who knows about it, what it may rewrite, and which headers you can trust.

Conceptual
▶ InteractiveInterview question
Progress

The problem

Your browser thinks it is connected to api.example.com. ss -tn on the server shows a connection from 10.0.3.7, which is not your address. Something in the middle terminated your TCP connection and opened its own. What is it, who put it there, and what did it do to the request on the way through?

Two connections, one request

A proxy is not a router. A router forwards IP packets without ending the connection; a proxy terminates a TCP (usually also TLS) connection from one side, reads the bytes as an application protocol, and opens a second connection to the other side. The two connections have different source addresses, different TCP state, possibly different HTTP versions, and independent lifetimes. Everything a proxy can do — cache, rewrite, route, buffer, compress, inspect — follows from the fact that it holds a complete request in its own memory between the two connections.

That also tells you what a proxy costs: one extra hop of latency (tens of microseconds in the same rack, a full RTT if it is elsewhere), one extra place to run out of file descriptors or ports (see The Connection Lifecycle: Close, Reset, TIME_WAIT, CLOSE_WAIT for TIME_WAIT on the proxy side), and one extra timeout that has to agree with the timeouts on either side of it.

What actually connects to what
  1. Client applicationopens TCP + TLS to whatever address DNS returned
  2. Connection 1: client ↔ proxyterminated at the proxy; TLS decrypted here if the proxy owns the certificate
  3. Proxy processparses the request; applies rules; may serve from cache
  4. Connection 2: proxy ↔ upstreamnew source IP (the proxy’s), often a pooled keep-alive connection
  5. Upstream serversees the proxy as its client unless told otherwise

Forward proxy: acting for the client

A forward proxy sits on the client’s side of the network and speaks to the outside world on the client’s behalf. The client is configured to use it (HTTP_PROXY=http://proxy.corp:3128, a PAC file, or a browser setting) and knows it exists. Uses: egress control (only the proxy is allowed through the firewall, so every outbound request is logged and filterable), caching shared across many clients, anonymity (the origin sees the proxy’s address), and policy — the corporate filter that blocks a domain category.

For plain HTTP the client sends the full URL in the request line (GET http://example.com/ HTTP/1.1) and the proxy fetches it. For HTTPS the client sends CONNECT example.com:443 and the proxy becomes a blind TCP tunnel: it sees the hostname (from the CONNECT line) but nothing inside the TLS session. A corporate proxy that wants to inspect HTTPS content must install its own CA on every client and re-sign certificates — the Certificates and the Chain of Trust chain of trust deliberately makes that impossible without the client’s cooperation.

Squid is the classic forward proxy; Envoy and nginx can be configured as one; the sidecar in a service mesh acts as a forward proxy for outbound traffic from its pod.

Reverse proxy: acting for the servers

A reverse proxy sits in front of server infrastructure and is, from the client’s point of view, the server. DNS for api.example.com resolves to the proxy; the client has no way to know there is anything behind it. nginx (proxy_pass), HAProxy, Envoy, Traefik and Caddy are reverse proxies; so is every cloud load balancer that terminates HTTP.

What a reverse proxy takes off the application: TLS termination (one place holds the certificate and the CPU cost of handshakes — see The TLS Handshake); routing (/api/* to one pool, /static/* to another, by Host header or path); caching and compression of responses; and buffering slow clients — nginx reads the whole upstream response into its own buffer and frees the application worker immediately, then drips it to a slow phone link over the next seconds. Without that, a 3G client holds an application thread for the entire transfer (What Happens When the Receiver Is Slow).

It also changes what the application sees. The TCP peer is the proxy. The request may have been HTTP/2 or HTTP/3 on the outside and is HTTP/1.1 on the inside. The scheme may have been https outside and http inside. Anything the application needs to know about the original client has to be carried in headers — which is where trust breaks.

Forward vs reverse: the same machinery, opposite loyalties
Forward proxyReverse proxy
Acts on behalf ofthe clientthe server fleet
Who configures itthe client (or its network)the service operator
Client aware?yes (explicit); no if transparentno — it *is* the server to the client
Origin seesthe proxy’s IPthe proxy’s IP
Typical jobsegress control, filtering, shared cache, anonymityTLS termination, routing, caching, compression, buffering, load balancing
ExamplesSquid, corporate proxies, mesh sidecar (outbound)nginx, HAProxy, Envoy, Traefik, cloud ALB/Application Gateway

Transparent proxies and the headers that leak through

A transparent proxy intercepts traffic without the client being configured for it: a firewall rule (iptables -t nat -j REDIRECT) or a policy route steers port 80/443 into a local proxy process. ISPs and hotel networks did this to HTTP for years; service meshes do it today — Istio’s init container installs REDIRECT rules so every connection from the pod passes through the Envoy sidecar without the application knowing. Transparent HTTPS interception without a trusted CA can only tunnel, not read.

Because the upstream sees the proxy’s address, proxies annotate the request. X-Forwarded-For: client, proxy1, proxy2 — each hop appends the address it saw. X-Forwarded-Proto: https says what the client originally used. Forwarded: for=203.0.113.9;proto=https;by=10.0.3.7 is the standardized form (RFC 7239). Via: 1.1 edge-cache-7 names the proxy and the HTTP version it spoke, mostly for loop detection.

The trust problem: the client can send any of these headers itself. curl -H "X-Forwarded-For: 127.0.0.1" reaches an application that trusts the leftmost value and now believes the request is local. The only sound rule is: trust exactly the hops you operate, counted from the right. nginx expresses this as set_real_ip_from 10.0.3.0/24; real_ip_header X-Forwarded-For; real_ip_recursive on;; Express as app.set("trust proxy", 1) (trust one hop). Rate limiters, audit logs and IP allow-lists that read the leftmost value are the classic vulnerability.

What the application receives when a client behind a corporate forward proxy hits a reverse proxy
GET /orders HTTP/1.1
Host: api.example.com
X-Forwarded-For: 203.0.113.9, 198.51.100.4   # client, then the corporate proxy; our edge appended nothing here
X-Forwarded-Proto: https
Via: 1.1 corp-proxy, 1.1 edge-nginx
X-Real-IP: 198.51.100.4                      # set by *our* nginx: the peer it actually saw
Connection: keep-alive                       # inside connection is pooled, HTTP/1.1, plain http

Key points

  • A proxy terminates one connection and opens another; it holds the whole request in between, which is what lets it cache, route, rewrite and buffer.
  • Forward proxy: configured by and working for the client; the origin sees the proxy. Reverse proxy: deployed by and working for the servers; the client cannot tell it is there.
  • HTTPS through a forward proxy is a CONNECT tunnel — the proxy sees the hostname, not the content, unless it holds a CA the client trusts.
  • The upstream sees the proxy’s IP and protocol; X-Forwarded-For / Forwarded / Via carry the original — and the client can forge them.
  • Trust proxy headers only for the hops you operate, counting from the right; never take the leftmost X-Forwarded-For as the client.
  • Every proxy adds a hop, a set of timeouts and a descriptor table that can be exhausted; these are the first things to check when the proxy is blamed.

Why does this exist?

Mechanisms are answers to constraints. Open each question before reading the answer.

Why terminate the connection instead of just forwarding packets?

Because a packet forwarder cannot see a request. Caching, routing by path, compression and buffering all require a complete HTTP message in memory, and that requires ending the TCP stream and reading it.

Why does a corporate proxy need a certificate installed on every laptop?

TLS is designed so that a middle party cannot read the session. The only way to inspect HTTPS is to be the endpoint — re-sign each site’s certificate with a CA the client already trusts. Without that CA the proxy can only tunnel CONNECT bytes it cannot read.

Why do proxies append to `X-Forwarded-For` rather than replace it?

So the chain is preserved: each hop adds the peer it saw. A replacing proxy would erase upstream hops; an appending one lets the application count trusted hops from the right and ignore the rest.

Forward vs reverse proxy

Forward proxy vs reverse proxy
Same box in the middle, opposite question: who configured it, and who is it hiding?
Conceptual
Client10.0.0.4 (office LAN)
Forward proxy10.0.0.1 · public 198.51.100.7
example.com93.184.216.34 (internet)
On the wire at this step
GET http://example.com/ HTTP/1.1
Host: example.com
Client is configured to use the proxy. The browser (or the OS proxy setting, or HTTPS_PROXY) points at proxy.corp:3128. The client knows the proxy exists; the server never will.
Who knows what
Client IP as seen by the server's socket
198.51.100.7 (the proxy)
Real client IP available to the server
only if the proxy adds X-Forwarded-For
Server IP as seen by the client
connects to 10.0.0.1:3128, names example.com
Who terminates TLS
the server (proxy tunnels CONNECT)
Who chose to use the proxy
The client (explicit configuration).
1/5 · Client is configured to use the proxy

How it fails

What the failure looks like from inside real software.

  • Rate limiter keyed on the leftmost X-Forwarded-For value: an attacker rotates a forged header and never hits the limit; a legitimate corporate network with one proxy IP is throttled as one user.
  • Application generates http:// redirect URLs because it sees plain HTTP inside the proxy and was never told X-Forwarded-Proto — every redirect drops users out of TLS.
  • Proxy keep-alive timeout longer than the upstream’s: the proxy reuses a connection the upstream has just closed and returns 502 for a request the client did nothing wrong with.
  • Proxy on a single host runs out of ephemeral ports to the upstream (EADDRNOTAVAIL) under load because every client connection became a fresh upstream connection — no connection pool.
  • Transparent interception of HTTPS by a hotel network: every site shows a certificate warning until the captive portal is accepted; developers see SSL_ERROR_BAD_CERT_DOMAIN from curl and blame the server.
  • Response buffering disabled for a streaming endpoint by accident, or enabled for one that needs streaming: SSE clients see nothing until the proxy buffer fills or the response ends.