Quick answer
A reverse proxy is a server that receives requests intended for your application, then decides what to do with them — serve from cache, block, challenge, or forward to one of your origin servers. To the visitor it looks like the website itself; your actual servers stay unpublished behind it. Nearly every CDN, WAF and load balancer is a reverse proxy with a particular emphasis, which is why one system can be all three.
01Forward proxy vs. reverse proxy
Both sit in the middle of a connection, but they act for opposite parties. A forward proxy acts for the client: your browser sends requests through it, and the destination server sees the proxy's address instead of yours. A VPN or a corporate web filter is a forward proxy.
A reverse proxy acts for the server: visitors address it directly, believing it to be the site, and it decides what reaches the real infrastructure behind. The client usually has no idea it exists, although response headers often give it away; the HTTP headers checker shows which proxy or CDN answers for any site.
| Forward proxy | Reverse proxy | |
|---|---|---|
| Acts for | The client | The server |
| Hides | The visitor from the site | The infrastructure from the visitor |
| Configured by | The user or their network | The site operator |
| Typical use | VPN, corporate filtering | CDN, WAF, load balancer |
02What a reverse proxy does with a request
- Terminates TLS, so certificates are managed in one place rather than on every backend
- Inspects the request and may block, challenge or rate limit it before your application is involved
- Serves it from cache if a fresh copy exists, without contacting the origin at all
- Chooses a backend and forwards it, balancing load and skipping unhealthy servers
- Rewrites headers, compresses responses and normalises anything the origin should not have to handle
03A minimal reverse proxy configuration
In nginx, a working reverse proxy is a couple of dozen lines. The details that matter are the ones people skip. nginx sends the upstream's own name as the Host header unless told otherwise, so name-based virtual hosts on the origin see the wrong site. And it does not pass the WebSocket Upgrade and Connection headers on by default, because they are hop-by-hop, so WebSocket routes need them set explicitly.
Two lines in the example exist for older versions. nginx 1.29.7 made HTTP/1.1 to the upstream the default; earlier releases need proxy_http_version 1.1 and an empty Connection header before upstream keepalive connections work.
upstream app {
server 10.0.0.10:8080;
server 10.0.0.11:8080;
keepalive 16;
}
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/ssl/example.com/fullchain.pem;
ssl_certificate_key /etc/ssl/example.com/privkey.pem;
location / {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /ws/ {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}04Why hiding the origin matters
Once traffic goes through a reverse proxy, your origin only ever needs to accept connections from the proxy. Everything else can be firewalled off entirely, which means an attacker who wants to reach your application has to go through the layer designed to stop them.
The protection collapses if the origin address leaks, and it leaks more often than people expect: old DNS records that were never deleted, certificate transparency logs, mail headers sent by the same host, error pages that print an internal hostname, or a subdomain that was never proxied. The guide to hiding your origin IP works through each of them.
05Locking the origin down
On a Linux origin the lock is a small firewall table: accept ports 80 and 443 only from your proxy provider's published ranges, for IPv4 and IPv6, drop them from everywhere else, and leave SSH and other ports to your existing rules. A tested nftables ruleset that also covers UDP 443 for HTTP/3 is in the origin lock-down guide; refresh its ranges whenever your provider's list changes.
Then test from an outside address that is not on the list. The connection should time out rather than reach your application, while requests through the proxy keep working.
06Keeping the visitor's real address
Because the proxy makes the connection to your origin, your application sees the proxy's address as the client; the visitor's address arrives in a header instead, usually X-Forwarded-For or the standardised Forwarded header from RFC 7239. That header must only be trusted on connections that genuinely came from your proxy, or anyone can claim any address and slip past every rate limit and geo rule you have. Which address in the list to believe is explained in the X-Forwarded-For guide, and a tested origin setup in nginx real IP.
07Troubleshooting a reverse proxy
| Symptom | Usual cause | Fix |
|---|---|---|
| 502 Bad Gateway | The origin is down, refused the connection, or failed the TLS handshake with the proxy | Test the origin from the proxy's address; see what causes a 502 |
| 504 Gateway Timeout | The origin accepted the request but did not answer in time | Find the slow endpoint before raising timeouts; see how to fix a 504 |
| Redirect loop after moving TLS to the proxy | The origin sees plain HTTP and keeps redirecting to HTTPS | Send X-Forwarded-Proto and have the application trust it from the proxy; see redirect loops behind a proxy |
| Every visitor has the same IP in the logs | The real client address is not restored | Configure the realip module or your framework's trusted-proxy setting |
| The wrong site or a default page appears | The Host header carries the upstream name, not the visitor's | proxy_set_header Host $host |
| WebSockets disconnect or never connect | Upgrade and Connection not forwarded, or the idle connection timed out | Pass both headers explicitly; nginx closes a proxied connection after 60 seconds without data unless proxy_read_timeout is raised or the application sends pings |
Questions
Frequently asked questions
Sources
Sources and verification
nginx configuration checked with nginx -t (nginx 1.22.1, self-signed test certificates), Debian 12, October 2026.
Last updated