Reverse Proxy Fundamentals — Complete Guide
In this tutorial, you'll learn about Reverse Proxy. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
A reverse proxy is a server that sits between clients and backend servers, forwarding client requests to the appropriate server and returning the response to the client.
What You'll Learn
By the end of this lesson, you will understand how a reverse proxy differs from a forward proxy, how it forwards requests, terminates SSL, and distributes load.
Why It Matters
Every API Gateway is built on reverse proxy technology. Understanding reverse proxy fundamentals is essential before learning about gateway-specific features.
Real-World Use
NGINX serves as a reverse proxy for high-traffic websites, forwarding requests to application servers, Caching responses, and terminating SSL certificates.
Forward vs Reverse Proxy
flowchart LR
subgraph "Forward Proxy"
C1[Client] --> FP[Forward Proxy]
FP --> I1[Internet]
end
subgraph "Reverse Proxy"
C2[Client] --> RP[Reverse Proxy]
RP --> S1[Server 1]
RP --> S2[Server 2]
end
A forward proxy acts on behalf of clients to access the internet. A reverse proxy acts on behalf of servers to receive client requests.
Basic Reverse Proxy Implementation
# reverse_proxy_basic.py
import http.server
import urllib.request
import json
from typing import Optional
class SimpleReverseProxy:
"""A basic reverse proxy that forwards requests to backend servers."""
def __init__(self):
self.backends = {}
self.current_backend = 0
def add_backend(self, name: str, url: str):
self.backends[name] = url
def forward_request(self, method: str, path: str,
headers: dict, body: Optional[str] = None) -> dict:
backend_name = list(self.backends.keys())[self.current_backend % len(self.backends)]
backend_url = self.backends[backend_name]
self.current_backend += 1
forwarded_path = f"{backend_url}{path}"
return {
"forwarded_to": forwarded_path,
"backend": backend_name,
"method": method,
"headers_added": {
"X-Forwarded-For": "gateway",
"X-Forwarded-Host": "api.example.com",
},
}
proxy = SimpleReverseProxy()
proxy.add_backend("web-1", "http://10.0.0.1:8080")
proxy.add_backend("web-2", "http://10.0.0.2:8080")
for i in range(4):
result = proxy.forward_request("GET", "/api/users", {})
print(f"Request {i+1}: -> {result['backend']} ({result['forwarded_to']})")
Expected output:
Request 1: -> web-1 (http://10.0.0.1:8080/api/users)
Request 2: -> web-2 (http://10.0.0.2:8080/api/users)
Request 3: -> web-1 (http://10.0.0.1:8080/api/users)
Request 4: -> web-2 (http://10.0.0.2:8080/api/users)
SSL Termination
A reverse proxy can handle SSL/TLS decryption, reducing the load on backend servers.
# ssl_termination.py
import ssl
from typing import Optional
class SSLTerminator:
"""Demonstrate SSL termination at the reverse proxy."""
def __init__(self, cert_path: str, key_path: str):
self.cert_path = cert_path
self.key_path = key_path
self.internal_protocol = "http"
def terminate_ssl(self, client_connection: dict) -> dict:
decrypted = client_connection.copy()
decrypted["scheme"] = "http"
decrypted["ssl_terminated"] = True
decrypted["tls_version"] = "TLS 1.3"
self._add_forwarded_headers(decrypted)
return decrypted
def _add_forwarded_headers(self, connection: dict):
connection.setdefault("headers", {})
connection["headers"]["X-Forwarded-Proto"] = "https"
connection["headers"]["X-Forwarded-Ssl"] = "on"
def forward_to_backend(self, connection: dict, backend_url: str) -> dict:
return {
"original_client_ip": connection.get("client_ip"),
"forwarded_to": backend_url,
"protocol": self.internal_protocol,
"ssl_terminated": connection.get("ssl_terminated", False),
"tls_version": connection.get("tls_version"),
}
terminator = SSLTerminator("/etc/certs/cert.pem", "/etc/certs/key.pem")
client = {
"client_ip": "203.0.113.42",
"scheme": "https",
"headers": {"Host": "api.example.com"},
}
decrypted = terminator.terminate_ssl(client)
result = terminator.forward_to_backend(decrypted, "http://backend:3000")
print(json.dumps(result, indent=2))
Expected output:
{
"original_client_ip": "203.0.113.42",
"forwarded_to": "http://backend:3000",
"protocol": "http",
"ssl_terminated": true,
"tls_version": "TLS 1.3"
}
Headers Added by Reverse Proxy
X-Forwarded-For: original client IP
X-Forwarded-Host: original Host header
X-Forwarded-Proto: original protocol (http/https)
X-Real-IP: client IP (NGINX specific)
Common Mistakes
1. Not Forwarding Client IP
Without X-Forwarded-For headers, backend servers see the proxy IP as the client. Always forward the original client IP.
2. Exposing Backend Directly
Backend servers should only accept connections from the reverse proxy, not from the internet directly.
3. Ignoring Buffer Sizes
Proxy buffers that are too small cause request failures for large payloads. Configure appropriate proxy buffer sizes.
4. No Health Checks
A reverse proxy that routes to unhealthy servers causes errors. Always configure health checks.
5. Session Affinity Without Sticky Sessions
If sessions are stored locally, requests must go to the same server. Configure sticky sessions or use a shared session store.
Practice Questions
1. What is the difference between a forward proxy and a reverse proxy?
A forward proxy acts for clients to access the internet. A reverse proxy acts for servers to receive client requests.
2. Why should SSL be terminated at the reverse proxy?
It offloads CPU-intensive decryption from backend servers and centralizes certificate management at one point.
3. What is the X-Forwarded-For header used for?
It carries the original client IP address through the proxy chain so backend servers know who the real client is.
4. How does a reverse proxy improve security?
It hides internal server details, terminates SSL, and provides a single point for access control and filtering.
Challenge
Configure a reverse proxy that handles three backend services, terminates SSL, adds proper forwarding headers, and performs health checks before routing.
FAQ
Mini Project: Simple Reverse Proxy
# simple_proxy.py
import json
from typing import Optional
class ProxyBackend:
def __init__(self, name: str, url: str, healthy: bool = True):
self.name = name
self.url = url
self.healthy = healthy
self.request_count = 0
class RoundRobinProxy:
def __init__(self):
self.backends = []
def add_backend(self, name: str, url: str):
self.backends.append(ProxyBackend(name, url))
return len(self.backends) - 1
def get_next_healthy(self):
healthy = [b for b in self.backends if b.healthy]
if not healthy:
return None
backend = healthy[0]
healthy[0].request_count += 1
return backend
def forward(self, request: dict) -> dict:
backend = self.get_next_healthy()
if not backend:
return {"status": 503, "body": "No healthy backends"}
forwarded_headers = {
"X-Forwarded-For": request.get("client_ip", "unknown"),
"X-Forwarded-Method": request.get("method", "GET"),
}
return {
"status": 200,
"forwarded_to": f"{backend.url}{request.get('path', '/')}",
"backend": backend.name,
"headers": forwarded_headers,
}
proxy = RoundRobinProxy()
proxy.add_backend("app-server-1", "http://10.0.0.1:3000")
proxy.add_backend("app-server-2", "http://10.0.0.2:3000")
for i in range(3):
result = proxy.forward({"method": "GET", "path": "/api", "client_ip": f"client_{i}"})
print(f"Request {i+1}: -> {result['backend']} ({result['forwarded_to']})")
Expected output:
Request 1: -> app-server-1 (http://10.0.0.1:3000/api)
Request 2: -> app-server-2 (http://10.0.0.2:3000/api)
Request 3: -> app-server-1 (http://10.0.0.1:3000/api)
What's Next
You understand reverse proxy basics. Next, learn about request routing, then explore load balancing patterns.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro