CDN & Reverse Proxy
Why this matters
Module 06 put a load balancer between clients and your servers. That box is
one kind of proxy: a host that accepts a connection, makes a second
connection of its own, and relays between them. Proxies show up on both sides
of almost every real connection. A forward proxy works for the clients:
the corporate egress proxy your laptop, CI runners and containers must go
through to reach the internet. A reverse proxy works for the servers:
the NGINX, Envoy or cloud gateway that terminates TLS and hides your backends.
Mixing the two up is a classic interview slip. In practice it also leads to
real debugging mistakes: you blame the app for a 502 the proxy generated, you
read the proxy's IP as the client's, or you spend an hour on a container that
"has no internet" when it was simply never told about the egress proxy.
This module is about the two kinds of proxy at the network level: who configures each one, what the request looks like on each hop, and what each hides. A CDN is a distributed reverse proxy with a cache, and it gets a short section here that points to its full treatment in the backend track.
Concepts
One question tells them apart: who is it working for?
Both kinds sit in the middle of a connection and relay traffic. The difference is whose agent the proxy is, and that decides everything else:
forward proxy (works for clients) reverse proxy (works for servers)
client ─┐ ┌─► backend 1
client ─┼─► forward proxy ─► any site any client ─► reverse ┼─► backend 2
client ─┘ (your company's) on the proxy (the site's) └─► backend 3
internet own front door)
hides: which client asked hides: which backend answered, and how many exist
configured by: the client side configured by: the server side (DNS points at it)
- Forward proxy. The client is configured to use it (browser setting,
HTTPS_PROXYvariable, a PAC file), and it can reach any destination the policy allows. The destination sees the proxy's IP, not the client's. - Reverse proxy. The client has no idea it's there: DNS for
api.example.comsimply resolves to the proxy, which forwards to a backend it chooses. The client sees one address; the backends are never reachable directly.
A quick test when you meet an unfamiliar proxy: if the client had to be told about it, it's a forward proxy. If the client just connected to the site's public address, it's a reverse proxy.
Forward proxies: egress control
Companies put forward proxies on the path out of their network for reasons that are all about the client side:
- Egress control and filtering. Only the proxy may talk to the internet; the firewall (module 05) blocks direct outbound traffic, and the proxy enforces an allow-list of domains. This is how a security team stops a compromised build server from calling an attacker's server.
- Auditing. Every outbound request is logged in one place.
- Caching shared downloads. Many machines fetching the same package hit the proxy's copy.
- A stable source IP. Partners can allow-list the proxy's one public address instead of every machine behind it (the same effect NAT gives you, but at the application layer).
On the wire, a client using a forward proxy connects to the proxy, not the destination. For plain HTTP it sends the full URL in the request line, so the proxy knows where to go:
GET http://example.com/ HTTP/1.1 ← absolute URL, not just "/"
Host: example.com
For HTTPS the proxy can't read the request, so the client asks it to open a
raw TCP tunnel with CONNECT, and then runs TLS with the real server
through that tunnel:
CONNECT example.com:443 HTTP/1.1
Host: example.com:443
HTTP/1.1 200 Connection established ← from the proxy; now TLS starts end to end
The proxy sees the destination name and port but not the content. (Some corporate proxies do TLS interception: they terminate TLS with their own certificate, which works only because the company installed its private CA in every laptop's trust store. Containers and CI images that lack that CA fail with "unable to get local issuer certificate".)
Most command-line tools and language runtimes pick up a forward proxy from
environment variables: HTTP_PROXY, HTTPS_PROXY and NO_PROXY (a list of
hosts and suffixes to reach directly, usually internal names and
localhost). Lowercase versions are also read, inconsistently, by different
tools. A container does not inherit your shell's variables, so "works on my
laptop, times out in Docker" is very often just a missing HTTPS_PROXY.
Reverse proxies: the servers' front door
A reverse proxy sits in the same position as module 06's load balancer, and real software (NGINX, HAProxy, Envoy, Traefik, cloud application gateways) does both jobs at once. What it takes off your app servers:
- TLS termination in one place, so certificates live on the proxy and backends speak plain HTTP on the internal network (TLS, SSL and HTTPS covers where to terminate).
- Load balancing and health checks across backend instances (module 06).
- Buffering slow clients. The proxy absorbs a slow mobile upload or download so an app worker isn't tied up for its whole duration. This is why Python app servers like Gunicorn are always run behind NGINX.
- Compression, caching, rate limiting, request routing by host or path.
- Hiding topology. Probing the public address tells an attacker nothing about how many backends exist or where they are.
The cost of that hiding: the backend's TCP connection comes from the proxy,
so its socket shows the proxy's IP for every request. The proxy has to pass
the original client details along as headers: X-Forwarded-For (client IP,
with each proxy appending to the list), X-Forwarded-Proto (was it HTTPS?),
X-Forwarded-Host, or the standard Forwarded header. A backend must trust
those headers only from its own proxy, since any client can send a fake
X-Forwarded-For. Configuring this in NGINX, line by line, is in
Nginx: reverse proxy and load balancing.
A common production chain combines several reverse proxies:
client → CDN edge → NGINX → Gunicorn → Uvicorn workers. Each hop is a
separate TCP (and maybe TLS) connection, with its own timeouts and its own logs.
Reading failures through a proxy
Because a proxy holds two connections, it can fail on either side, and it answers the client itself when the backend side breaks. Learn to recognise responses the proxy generated:
502 Bad Gateway: the proxy reached for a backend and got nothing usable: connection refused, connection reset, or a malformed reply. The app is usually down or listening on the wrong port.504 Gateway Timeout: the backend accepted the connection but didn't answer within the proxy's timeout. The app is slow or stuck.503 Service Unavailable: often the proxy has no healthy backend left in the pool.407 Proxy Authentication Required: a forward proxy wants credentials before it will relay your request.
The error page's Server: header and styling usually name the proxy (an
nginx page means NGINX answered, not your app). Then do what module 06
taught: skip the proxy and hit the backend directly (its own port, or
curl --resolve to its IP). If the backend works directly, the problem is in
the proxy hop: config, timeout, or a header it rewrote or dropped.
Where CDNs fit
A CDN is a reverse proxy with a cache, run by someone else at hundreds of
points of presence close to your users. Everything above applies (it
terminates TLS, hides your origin, and adds X-Forwarded-For-style headers),
and on top of that it serves cached copies so most requests never reach you.
Cache hits and misses, cache keys, purging, origin shielding and reading a
real CDN's Age/X-Cache headers are taught in one place:
CDN and edge caching.
Command reference
| Command | What it does | Example |
|---|---|---|
curl -x |
Sends a request through a forward proxy | curl -x http://localhost:3128 -v http://example.com/ |
HTTPS_PROXY / NO_PROXY |
Environment variables most tools read to find a forward proxy | HTTPS_PROXY=http://localhost:3128 curl -sI https://example.com/ |
curl --noproxy |
Bypasses any configured proxy for the listed hosts | curl --noproxy '*' -sI https://example.com/ |
curl -v (through a reverse proxy) |
Shows which server actually answered (Server:, Via: headers) |
curl -sv http://localhost:8080/ -o /dev/null |
curl --resolve |
Skips DNS and the front proxy to hit a specific backend IP | curl --resolve api.example.com:443:10.0.1.5 https://api.example.com/ |
env | grep -i proxy |
Shows which proxy variables a shell or container actually has | docker exec app env | grep -i proxy |
Flag/output notes:
curl -x http://localhost:3128 -v http://example.com/—-x(or--proxy) names the forward proxy. In-voutput, curl connects tolocalhost:3128, not toexample.com, and the request line carries the absolute URL. For anhttps://URL,-vshowsCONNECT example.com:443followed by200 Connection establishedand then the normal TLS handshake.curl --noproxy '*'— ignore every proxy variable for this request. Useful to prove whether a failure comes from the proxy or the destination.Via:— a header some proxies add naming themselves; together withServer:it tells you which hop produced a response.
Hands-on exercises
You need Docker (from the Docker track). The forward-proxy exercises use the
ubuntu/squid image; the reverse-proxy ones use NGINX and a small echo
server that prints back the request it received.
Run a forward proxy and use it explicitly. Start Squid:
docker run -d --name fwd -p 3128:3128 ubuntu/squid. Then runcurl -x http://localhost:3128 -v http://example.com/ -o /dev/null. Expected: curl'sConnected to localhost ... port 3128line (not example.com), and a request line ofGET http://example.com/ HTTP/1.1. Checkdocker logs fwdordocker exec fwd tail /var/log/squid/access.logto see the proxy's log entry for your request.Watch the CONNECT tunnel for HTTPS. Run
curl -x http://localhost:3128 -v https://example.com/ -o /dev/null 2>&1 | grep -E 'CONNECT|established|SSL connection|subject'. Expected:CONNECT example.com:443, then200 Connection establishedfrom the proxy, then a TLS handshake whose certificate is example.com's. The proxy relayed encrypted bytes it could not read; its access log shows onlyCONNECT example.com:443.Use the environment variables instead. Run
HTTPS_PROXY=http://localhost:3128 curl -sI https://example.com/and confirm a newCONNECTline in the Squid log. Now addNO_PROXY=example.comto the same command and confirm the log gets no new entry, because curl went direct.Put a reverse proxy in front of an echo server. Create a network and a backend that echoes requests back as JSON:
docker network create rp && docker run -d --name echo --network rp mendhak/http-https-echo. Writenginx.conf:events {} http { server { listen 80; location / { proxy_pass http://echo:8080; } } }Start NGINX:
docker run -d --name rev --network rp -p 8080:80 -v "$PWD/nginx.conf:/etc/nginx/nginx.conf:ro" nginx. Runcurl -s localhost:8080/ | python3 -m json.tool | grep -E '"ip"|forwarded|host'. Expected: the backend reports the NGINX container's IP as the client, and nox-forwarded-forheader. From the backend's point of view, every user on earth is the proxy.Forward the client's identity. Add these lines inside the
locationblock, thendocker exec rev nginx -s reload:proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme;Repeat the
curl. Expected:x-forwarded-fornow carries your real address (the Docker bridge gateway, as seen from the proxy). Then send a fake one:curl -s -H 'X-Forwarded-For: 6.6.6.6' localhost:8080/. Expected: the header becomes6.6.6.6, <your address>. The proxy appends; it doesn't replace. That's why a backend must take the address its own proxy added, not the leftmost value a client could have forged.Diagnose and fix: a 502 that isn't the app's error. Stop the backend:
docker stop echo, thencurl -si localhost:8080/ | head -5. Expected:502 Bad GatewaywithServer: nginx. NGINX generated this response; your app never saw the request. Diagnose:docker logs revshowsconnect() failedorhost not foundfor the upstream. Check the backend directly (docker ps -ashowsechoexited). Fix:docker start echo, wait a moment, and confirmcurl localhost:8080/returns 200 again. If it still fails, restart NGINX too: it may have cached the old container IP forecho. The lesson is to read theServer:header before opening the app's logs.Diagnose and fix: "the container has no internet." Simulate an egress-only network: block direct outbound access from a test container by giving it a network with no route out, while the proxy can still reach the internet:
docker network create --internal locked docker network connect locked fwd docker run --rm --network locked curlimages/curl -sS -m 5 https://example.com/ -o /dev/nullExpected: a timeout or "could not resolve host". It looks like a broken network, but in a proxied environment it is the designed behaviour: only the proxy may go out. Fix: tell the container about the proxy:
docker run --rm --network locked -e HTTPS_PROXY=http://fwd:3128 curlimages/curl -sSI https://example.com/. Expected: a200response through Squid. In real environments, setHTTPS_PROXY/NO_PROXYin the Dockerfile, the Compose file or the Docker daemon's proxy config, not by hand.Clean up:
docker rm -f fwd rev echo && docker network rm rp locked.
Independent challenge
No lab given. A Python service runs on Kubernetes inside a corporate network.
It must call a partner's public API over HTTPS, and it is itself exposed to
users through an ingress reverse proxy. Three bugs are reported: (a) calls to
the partner API time out after 30 seconds, but the same curl works from a
developer's laptop; (b) once HTTPS_PROXY is set, the partner calls work but
calls to an internal service orders.svc.cluster.local start failing with
403 from the proxy; (c) the service's rate limiter blocks all users at
once, because it thinks every request comes from the same IP. For each, name
which proxy is involved (forward or reverse), the mechanism, and the fix.
Stuck? One hint
(a) is the forward proxy: the laptop has HTTPS_PROXY set (or a system proxy
setting) and the pod doesn't, so its direct outbound connection is dropped by
the egress firewall. Set HTTPS_PROXY in the pod spec. (b) is the same
forward proxy over-applied: with no NO_PROXY, internal calls also go to the
proxy, which refuses destinations outside its allow-list. Add
NO_PROXY=.svc.cluster.local,.cluster.local,localhost,127.0.0.1 (plus your
internal domains). (c) is the reverse proxy: the app is reading the socket's
peer address, which is always the ingress controller. Configure the ingress to
send X-Forwarded-For, and have the app trust that header only from the
ingress's address range.
Common mistakes & troubleshooting
- Confusing a reverse proxy with a forward proxy. Ask whose agent it is. A forward proxy hides clients and is configured on the client side; a reverse proxy hides servers and is reached through the site's own DNS.
- Treating a proxy's 502/504 as an application bug. Check the
Server:header and the proxy's own logs first, then hit the backend directly.502means the backend was unreachable or broken;504means it was too slow. - Logging or rate-limiting on the socket address behind a reverse proxy.
Every request appears to come from the proxy. Use
X-Forwarded-For, and trust it only from your own proxy. - Trusting the leftmost
X-Forwarded-Forvalue. Clients can send that header themselves. Take the entry your own proxy appended. - Forgetting containers don't inherit proxy settings. A shell's
HTTPS_PROXYisn't passed intodocker run, CI jobs or pods unless you set it there. - Setting
HTTPS_PROXYwithoutNO_PROXY. Internal traffic then goes to the egress proxy too, which is slow at best and refused at worst. - Missing the corporate CA in container images. With TLS interception, every HTTPS call fails certificate validation inside images that don't trust the company's private CA. Add the CA to the image's trust store; don't disable verification.
Checkpoint quiz
Write down your answer to each question before expanding it — checking without attempting first is the single easiest way to fool yourself into thinking you've learned this.
- What is the one question that separates a forward proxy from a reverse proxy, and what does each one hide?
- How does a client end up using a forward proxy, and how does it end up using a reverse proxy?
- What does an HTTP request line look like when sent through a forward proxy, and why is HTTPS handled differently?
- Name four jobs a reverse proxy takes off your application servers.
- Behind a reverse proxy, your app logs the same IP for every request. Why, and what's the fix?
- You get a
502 Bad GatewaywithServer: nginx. Who generated it, what does it usually mean, and what do you check first? - A build in a container times out fetching packages but works on your laptop on the same corporate network. What's the likely cause?
- How is a CDN related to a reverse proxy?
Show answers
- "Who is it working for?" A forward proxy works for clients and hides which client made the request from the destination. A reverse proxy works for servers and hides which backend answered, and how many backends exist, from the client.
- A forward proxy must be configured on the client side: browser or OS
settings, a PAC file, or
HTTP_PROXY/HTTPS_PROXYvariables. A reverse proxy needs nothing from the client: the site's DNS name resolves to the proxy, so the client connects to it without knowing. - For plain HTTP the request line carries the full URL
(
GET http://example.com/ HTTP/1.1) so the proxy knows where to forward it. For HTTPS the proxy can't read the encrypted request, so the client sendsCONNECT example.com:443, the proxy opens a TCP tunnel, and TLS runs end to end through it. The proxy sees only the destination host and port. - Any four: TLS termination, load balancing and health checks, buffering slow clients, compression, response caching, rate limiting, host/path routing, and hiding backend topology.
- The app's TCP connection comes from the proxy, so the socket's peer address
is always the proxy. Have the proxy add
X-Forwarded-For(andX-Forwarded-Proto), and have the app read it only when the request arrived from the proxy's own address. - NGINX generated it, not the application. It usually means NGINX could not get a usable response from the backend (refused, reset, or the backend is down or on the wrong port). Check the proxy's error log, then hit the backend directly to see whether it's up.
- The laptop has the corporate forward proxy configured, but the container
doesn't inherit those settings, and the egress firewall drops direct
outbound connections. Set
HTTPS_PROXY(andNO_PROXYfor internal hosts) in the container's environment. - A CDN is a reverse proxy with a cache, operated at many points of presence close to users. It terminates TLS and hides your origin like any reverse proxy, and also serves cached copies so most requests never reach you.
Further reading & sources
- MDN: Proxy servers and tunneling - forward vs reverse proxies, the
CONNECTmethod and proxy-related headers. - MDN: X-Forwarded-For - the header format and the security caveats of trusting it.
- RFC 7239: Forwarded HTTP Extension - the standard
Forwardedheader that replaces theX-Forwarded-*family. - Cloudflare: What is a reverse proxy? - a side-by-side diagram of forward and reverse proxies.
- Docker: Use a proxy server with the Docker CLI - passing
HTTP_PROXY/NO_PROXYinto containers and builds.
Free PDF notes
Download these notes as a printable PDF, with every diagram and interview questions: SD-B-017: Forward vs Reverse Proxy. Each section in one file: Phase 2, Networking (PDF).
Next
12 — Capstone project: put the whole toolkit — layering, addressing, DNS, TCP/UDP, routing/NAT/firewalls, routing protocols, VPNs, load balancing and proxies — to work diagnosing and fixing a chain of deliberately broken connectivity problems across a small multi-host lab. WebSockets and gRPC, which change how proxies and load balancers behave, are covered in the backend track: WebSockets and Server-Sent Events and gRPC fundamentals.