Reverse Proxy Headers¶
The problem¶
When a reverse proxy terminates SSL, the backend app receives a plain HTTP request. The app has no way of knowing the original request was HTTPS — so it generates http:// URLs for assets, redirects, and links.
The browser loaded the page over HTTPS, but the asset URLs point to HTTP. This is mixed content — browsers block it. In DevTools, blocked requests show "provisional headers" because the request was killed before it was sent.
Why it happens¶
The proxy strips the SSL layer. From the app's perspective:
- Scheme:
http(nothttps) - Host:
127.0.0.1orlocalhost(not your public domain) - Client IP: the proxy's IP (not the real visitor)
Any URL the app generates using these values will be wrong.
The solution¶
Two things need to happen:
1. The proxy forwards the original request info¶
The X-Forwarded-* headers are a standard convention for reverse proxies to pass along what the original request looked like before the proxy modified it.
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
| Header | What it tells the app |
|---|---|
X-Forwarded-For |
The real client IP address (not the proxy's IP) |
X-Forwarded-Proto |
The original scheme — https or http |
X-Forwarded-Host |
The original hostname the browser used |
X-Forwarded-Port |
The original port (443 for HTTPS, 80 for HTTP) |
2. The app opts in to trusting those headers¶
Frameworks don't read X-Forwarded-* headers by default. This is a security measure — if the app were hit directly (no proxy in front), a malicious client could send fake X-Forwarded-Proto: https headers to trick the app.
So the app must explicitly say "I'm behind a proxy, trust these headers."
Laravel 10 and below — app/Http/Middleware/TrustProxies.php:
Laravel 11+ — bootstrap/app.php:
Django — settings.py:
Rails — reads X-Forwarded-Proto automatically when behind a proxy.
Next.js / Express — reads X-Forwarded-Proto automatically.
Is proxies = '*' safe in production?¶
'*' means "trust forwarded headers from any source." This is safe when the app is only reachable through a proxy, which is the typical setup (tunnel, load balancer, CDN).
If the app is directly reachable (no proxy), a client could spoof the headers. To guard against this, you can trust specific proxy IPs instead:
In practice, most production deployments (Forge, Vapor, any cloud LB) use '*' because the app sits behind infrastructure that's the only ingress point.