SSL redirect behind a proxy
This is the control that produces the most confusing failure in the whole cluster, because getting it half-right yields an infinite redirect loop and getting it wrong-but-working yields a security hole that looks fine.
SECURE_SSL_REDIRECT defaults to False; SECURE_PROXY_SSL_HEADER defaults to
None.
Why they have to be set together
Section titled “Why they have to be set together”SECURE_SSL_REDIRECT redirects any request where request.is_secure() is false. By
default is_secure() just asks whether the URL scheme is https.
Behind a proxy that terminates TLS, Django receives plain HTTP. So is_secure() is
always False, so Django redirects to HTTPS, so the proxy terminates TLS and forwards
plain HTTP again, so is_secure() is False. That is the loop, and it is the single
most common Django deployment failure.
SECURE_PROXY_SSL_HEADER is how Django learns the truth from the proxy.
SECURE_SSL_REDIRECT = TrueSECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")The header name is in request.META format — uppercase, underscores, and prefixed
with HTTP_, which Django adds to incoming headers automatically. "X-Forwarded-Proto"
in that tuple silently never matches.
Django accepts the header if its value is https, or if the leftmost value of a
comma-separated list is https (e.g. https,http,http) — worth knowing when there is
more than one proxy in the chain.
curl -sI http://example.com/ | head -1curl -s https://example.com/ -o /dev/null -w '%{http_code}\n'The first should be a 301 to https://, the second a 200. If the second is also a
redirect, you have the loop.
Your SECURE_SSL_REDIRECT setting is not set to True. Unless your site should be available over both SSL and non-SSL connections, you may want to either set this setting True or configure a load balancer or reverse-proxy server to redirect all connections to HTTPS.Django is not redirecting HTTP to HTTPS. Note the check itself offers the alternative — doing it at the proxy is an equally valid answer, and often the better one.
Read in: django/core/checks/security/base.py — id security.W008The header is a promise the proxy has to keep
Section titled “The header is a promise the proxy has to keep”Django’s documentation carries an explicit warning on this setting: “Modifying this setting can compromise your site’s security. Ensure you fully understand your setup before changing it.” It then requires all of the following to be true:
- the app is genuinely behind a proxy;
- the proxy strips
X-Forwarded-Protofrom all incoming requests — so a client cannot send its own; - the proxy sets
X-Forwarded-Protoitself and only sendshttpsfor real HTTPS.
The second condition is the one that gets missed. If the proxy passes a
client-supplied X-Forwarded-Proto: https through untouched, then any attacker can
make is_secure() return True for a plaintext request — which re-enables Secure
cookies over plaintext and defeats the redirect. The setting is only as trustworthy as
the proxy’s stripping rule.
This is the seam described on application vs server: the application side is one line, and it is worthless without the server side.
Enabling the redirect without the proxy header is the loop. Enabling the proxy header without the proxy stripping it is the hole. Deploy both sides together.
SECURE_REDIRECT_EXEMPT takes a list of regexes for paths that must stay reachable
over HTTP — health checks that predate TLS are the usual case. Use it sparingly; every
entry is a path where the redirect no longer applies.
Related
Section titled “Related”- Session and CSRF cookie flags — the other thing that breaks behind a proxy
- Security headers — HSTS belongs here once the redirect works
- Application vs server — the proxy half of this control, and the two other places the same seam shows up