Reverse Proxies and TLS Termination
A reverse proxy can simplify certificate management, publish private services, route multiple applications through one address, and add health checks or access controls. It can also create confusing failures when the application no longer knows the original scheme, hostname, or client address.
Where the Reverse Proxy Sits
Internet client
|
v
DNS and public IP
|
v
Firewall / NAT
|
v
Reverse proxy or load balancer
| | |
v v v
App A App B App C
The client connects to the proxy. The proxy chooses an upstream and creates a second connection to the application. These are separate transport and TLS sessions, even when the user experiences them as one website.
Common uses
- Publish multiple applications using name-based virtual hosting.
- Terminate public TLS certificates in one place.
- Keep application servers on private addresses.
- Load-balance across multiple upstreams.
- Add authentication, rate limits, security headers, or maintenance pages.
- Perform blue/green or migration cutovers.
- Normalize old applications that cannot manage modern TLS themselves.
Understand the Trust Boundaries
A proxy receives untrusted client input and then creates headers that the upstream may trust. The upstream must distinguish headers set by the trusted proxy from headers supplied directly by a client.
Client to proxy
Public TLS policy, certificate, HTTP limits, WAF or rate-limit behavior, and source-address handling apply here.
Proxy to upstream
May be HTTP on a private network, HTTPS using public or private trust, a Unix socket, FastCGI, or another protocol.
Management plane
Configuration API, dashboard, SSH, certificate storage, and reload permissions should not be exposed like the public listener.
Application trust
The application should trust forwarded headers only from known proxy addresses or networks.
Do not “fix” internal TLS by disabling verification
If the proxy uses HTTPS to an upstream, verify the upstream certificate. A name mismatch or private certificate should be solved by using the correct SNI name, a private CA trust store, or a valid internal certificate. Disabling verification creates encryption without identity and hides configuration mistakes.
Forwarded Headers
| Header | Purpose | Risk when wrong |
|---|---|---|
Host | Original requested hostname | Wrong virtual host, redirects, certificate assumptions, or tenant selection |
X-Forwarded-For | Client and proxy address chain | Incorrect logging, access control, or rate limiting |
X-Forwarded-Proto | Original scheme such as HTTPS | Redirect loops, mixed content, insecure cookies |
X-Forwarded-Host | Original public host | Incorrect generated URLs |
Forwarded | Standardized forwarding information | Same trust concerns as the X-Forwarded family |
| PROXY protocol | Transport-level source address metadata | Listener failure or spoofing if accepted from untrusted sources |
The mixed-content failure pattern
Browser requests: https://monitor.example.com/
Proxy connects upstream: http://10.10.10.25:8080/
Application believes: request scheme is HTTP
Application generates: http://monitor.example.com/assets/app.css
Browser blocks asset: mixed content
The fix is usually to pass and trust the original HTTPS state, then configure the application or FastCGI environment to use it. Simply rewriting every response body is fragile.
TLS Termination Patterns
Terminate at the proxy, use HTTP internally
This is simple and avoids internal certificate management, but the internal network path must be appropriately trusted and isolated.
Client -- HTTPS --> Proxy -- HTTP --> Application
Terminate and re-encrypt
Use when internal traffic crosses less-trusted networks, policy requires encryption, or the application identity must be verified end to end.
Client -- HTTPS --> Proxy -- HTTPS --> Application
| |
public cert public or private CA cert
TLS pass-through
The proxy or load balancer forwards encrypted traffic without terminating it. This preserves application-controlled TLS but limits HTTP-aware routing and header manipulation.
Certificate operations
- Know which system requests and renews each certificate.
- Monitor expiration from outside the proxy.
- Protect private keys and restrict configuration access.
- Validate full certificate chains and SNI behavior.
- Plan ACME HTTP-01 or DNS-01 validation paths.
- Document how certificates are restored after a proxy rebuild.
- Remove unused certificates and old hostnames.
Configuration Examples
Caddy: basic reverse proxy
app.example.com {
reverse_proxy 10.20.30.40:8080
}
With a public hostname and reachable validation path, Caddy can automate public certificate issuance and renewal. The application still needs correct proxy awareness.
Caddy: HTTPS upstream with private trust
app.example.com {
reverse_proxy https://10.20.30.40:8443 {
transport http {
tls_server_name app-internal.example.net
tls_trust_pool file /etc/caddy/internal-ca.pem
}
}
}
Caddy: multiple upstreams and health checking
app.example.com {
reverse_proxy 10.20.30.41:8080 10.20.30.42:8080 {
health_uri /healthz
lb_try_duration 5s
}
}
NGINX: basic proxy with forwarding headers
server {
listen 443 ssl;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
location / {
proxy_pass http://10.20.30.40:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
NGINX: WebSocket-capable location
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl;
server_name realtime.example.com;
location / {
proxy_pass http://10.20.30.50:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
FastCGI behind an HTTPS proxy
When NGINX or another web server passes a request to PHP-FPM, ensure the HTTPS state and forwarded protocol reach PHP correctly.
fastcgi_param HTTPS on;
fastcgi_param HTTP_X_FORWARDED_PROTO https;
fastcgi_param HTTP_X_FORWARDED_SSL on;
Only set these unconditionally when that server block is reached exclusively through the trusted HTTPS proxy path. Otherwise derive values safely from trusted input.
Avoid Making the Proxy the New Single Point of Failure
Publishing several redundant applications through one proxy moves the failure domain. Consider:
- Two proxy nodes with a virtual IP, routed service IP, or external load balancer
- Configuration synchronization and independent validation
- Certificate storage and renewal coordination
- Health checks that validate the actual application, not only the TCP port
- DNS failover limitations and TTL behavior
- Separate management and public interfaces
- Capacity for TLS handshakes, large uploads, streaming, and request buffering
Health checks need meaning
A health endpoint that always returns 200 even when the database is unavailable sends traffic to a broken node. A health endpoint that performs every expensive dependency check on every request may create load. Design separate liveness and readiness semantics where the application supports them.
Troubleshooting by Status and Layer
| Symptom | Likely checks |
|---|---|
| 502 Bad Gateway | Upstream address, listener, firewall, DNS, socket permissions, TLS verification, application crash |
| 504 Gateway Timeout | Upstream response time, proxy timeouts, database delay, network loss, application deadlock |
| Redirect loop | X-Forwarded-Proto, application base URL, HTTP-to-HTTPS logic, CDN/proxy chain |
| Wrong client IP | Trusted proxies, forwarded-header order, PROXY protocol configuration |
| Wrong certificate | DNS target, SNI, default virtual host, certificate selection, stale proxy node |
| Large uploads fail | Client body limit, request buffering, upstream limits, timeout, temporary filesystem space |
| WebSocket disconnects | HTTP version, upgrade headers, idle timeout, intermediary support |
| Assets load over HTTP | Application proxy awareness, scheme headers, cached base URL, FastCGI parameters |
Useful tests
# Test public DNS and certificate presentation
dig app.example.com A
dig app.example.com AAAA
openssl s_client -connect app.example.com:443 -servername app.example.com
# Show response headers and redirect chain
curl -I https://app.example.com/
curl -IL https://app.example.com/
# Test the upstream directly from the proxy
curl -v http://10.20.30.40:8080/
curl -vk https://10.20.30.40:8443/
# Validate configuration before reload
caddy validate --config /etc/caddy/Caddyfile
nginx -t