65. Proxies: forward, reverse, CONNECT, and interception
65.1 The two directions
FORWARD PROXY (on the client's side; the client knows about it)
client -> [proxy] -> any server on the internet
uses: corporate egress control, filtering, caching, anonymity
REVERSE PROXY (on the server's side; the client does not know)
client -> [proxy] -> your back-end servers
uses: load balancing, TLS termination, caching, WAF (Ch 40)
Part XI covered reverse proxies. This section is about the forward direction, which developers meet as "why does this only fail on the corporate network?".
65.2 The CONNECT method
A forward proxy can't read HTTPS, so for TLS it builds a blind TCP tunnel:
Client -> proxy: CONNECT example.com:443 HTTP/1.1
Host: example.com:443
Proxy-Authorization: Basic ... (if required)
Proxy -> client: HTTP/1.1 200 Connection Established
-> from here the proxy blindly relays bytes; the TLS handshake happens
end-to-end between client and origin. The proxy sees only the hostname
(from CONNECT) and the traffic volume.
Consequences: the proxy can allow/deny by hostname but cannot inspect content — unless it performs TLS interception (below).
65.3 How clients find a proxy
| Mechanism | How |
|---|---|
| Environment variables | http_proxy, https_proxy, no_proxy (respected by curl, most CLIs, many SDKs) |
| PAC file | A JavaScript function FindProxyForURL(url, host) returning "PROXY host:port" / "DIRECT". Allows per-destination rules. |
| WPAD | Auto-discovery of the PAC file via DHCP option 252 or DNS. Convenient; historically a nice attack vector. |
| OS/browser settings | System proxy configuration |
no_proxy is a classic footgun: its syntax is inconsistent across tools
(leading dots, CIDR support, wildcard support all vary), so "it works in curl but
not in my app" is common.
65.4 TLS interception (corporate MITM)
To inspect HTTPS, an enterprise proxy terminates TLS and re-encrypts:
client ==TLS(corp CA cert)==> [proxy decrypts, inspects] ==TLS(real cert)==> origin
This requires installing the corporate root CA on every device. Effects you will hit:
- Certificate pinning breaks (apps that pin to a specific CA/key refuse to connect).
- Non-browser clients fail because they use their own trust store: Python
(
certifi), Node, Java (cacerts), Go, Docker, package managers. Each needs the corporate CA added separately — this is the single most common cause of "SSL certificate problem: unable to get local issuer certificate" in corporate environments. - mTLS to external services breaks (the proxy can't present your client cert).
- HTTP/3 usually isn't supported by the proxy, so QUIC gets blocked and clients fall back to TCP.
65.5 The Forwarded / X-Forwarded-* headers
Once a proxy is in the path, the back end sees the proxy's address. Identity is carried in headers:
X-Forwarded-For: 203.0.113.9, 198.51.100.7 (client, then each proxy, left to right)
X-Forwarded-Proto: https
X-Forwarded-Host: example.com
Forwarded: for=203.0.113.9;proto=https;host=example.com (RFC 7239, the standard one)
Security rule: these headers are client-supplied and trivially spoofed. Only trust them from known proxy IPs, and strip or overwrite them at your edge. Count from the right, using the number of proxies you actually operate. Getting this wrong turns your rate limiter and IP allowlist into decoration.
For TCP-mode (L4) proxies that can't add headers, the PROXY protocol (Ch 38.11) carries the original addresses ahead of the byte stream.
65.6 Real-world example
A team's CI pipeline failed only inside the corporate network with
x509: certificate signed by unknown authority. curl worked (it had been
configured with --cacert), the browser worked (OS trust store had the corporate
CA), but Go and Python did not. Root cause: TLS interception plus three separate
trust stores. Fix: bake the corporate CA into the CI container image and set
SSL_CERT_FILE/NODE_EXTRA_CA_CERTS/REQUESTS_CA_BUNDLE, plus a no_proxy
entry for internal registries.
Try it yourself
# use a proxy explicitly
export https_proxy=http://proxy.corp:3128
curl -v https://example.com 2>&1 | grep -iE 'CONNECT|Proxy|HTTP/1.1 200'
# what does my environment think?
env | grep -i proxy
# which CA store is a tool using?
curl -v https://example.com 2>&1 | grep -i 'CAfile\|CApath'
python3 -c "import certifi; print(certifi.where())"
# detect TLS interception: is the issuer your corporate CA?
echo | openssl s_client -connect example.com:443 2>/dev/null \
| openssl x509 -noout -issuer