Skip to content

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

Browser --HTTPS--> Reverse Proxy --HTTP--> App

The proxy strips the SSL layer. From the app's perspective:

  • Scheme: http (not https)
  • Host: 127.0.0.1 or localhost (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 belowapp/Http/Middleware/TrustProxies.php:

protected $proxies = '*';

Laravel 11+bootstrap/app.php:

->withMiddleware(function (Middleware $middleware) {
    $middleware->trustProxies(at: '*');
})

Djangosettings.py:

SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

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:

protected $proxies = ['127.0.0.1', '10.0.0.0/8'];

In practice, most production deployments (Forge, Vapor, any cloud LB) use '*' because the app sits behind infrastructure that's the only ingress point.