HSTS
Enforcing HTTPS so the browser never makes a plain HTTP request
The problem with HTTPS alone
When a user types example.com into their browser, no protocol is specified. The browser defaults to plain HTTP. The server responds with a 301 redirect to https://example.com, the browser follows it, and the TLS handshake begins. From that point on, everything is encrypted.
The vulnerability is that first request - the one that went out over plain HTTP before the redirect came back. An on-path attacker (someone positioned between the user and the server - on café wifi, for example) can intercept that request and the redirect response. They can modify the redirect, strip it entirely, or respond themselves. This is called an SSL stripping attack. The user ends up on a plain HTTP session with the attacker in the middle, and has no way of knowing - the browser never got as far as showing a padlock.
The 301 doesn't help here. It travels over plain HTTP and is just as strippable as the original request.
What HSTS does
HTTP Strict Transport Security (HSTS) is a response header your server sends over HTTPS. When the browser sees it, it makes a note: for the next max-age seconds, never send a plain HTTP request to this domain - upgrade it to HTTPS locally, before it leaves the machine. The request never goes out over HTTP at all.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
- max-age - how long the browser caches this policy, in seconds. 31536000 is one year.
- includeSubDomains - applies the policy to all subdomains too.
- preload - a signal that you intend to submit to the preload list (more on this below).
This policy persists across browser sessions - it survives closing and reopening the browser. It also applies in incognito mode, which is worth knowing: incognito doesn't clear the HSTS cache, which is why HSTS is considered a minor fingerprinting vector (you can detect whether a user has previously visited a domain by checking whether their browser automatically upgrades requests to it).
The scenarios, from most to least secure
1. Domain is on the preload list
The browser ships with your domain baked into it. This isn't a remote API call - it's literally a file compiled into the browser binary (in Chrome, transport_security_state_static.json). Before the browser has ever visited your site, it already knows to use HTTPS. The very first request, ever, goes out over HTTPS. There is no plain HTTP window for an attacker to exploit.
2. Modern browser automatic upgrade (no HSTS required)
Chrome and Safari now automatically try HTTPS first for all navigations, regardless of whether a site has HSTS configured. For most users on modern browsers, the first-request vulnerability is already largely mitigated without any HSTS at all. This is why the maintainers of hstspreload.org themselves note that the preload list provides minimal additional benefit over the HSTS header alone - the browser is already doing most of the work. The gap only reopens when an active attacker specifically interferes with that automatic upgrade.
3. HSTS header, no preload
The first visit goes out over plain HTTP and is vulnerable to SSL stripping. The server responds with the HSTS header over HTTPS. Every subsequent visit - the browser upgrades to HTTPS locally before the request goes out, for the duration of max-age. Good protection for returning users; that first visit remains the weak point.
4. HTTPS with a 301 redirect, no HSTS
Every visit starts with a plain HTTP request and a redirect. Every visit has the SSL stripping window. The redirect offers no security guarantee because it travels over the same unencrypted channel it's trying to escape.
The preload list
Django's SECURE_HSTS_PRELOAD = True setting does exactly one thing: adds the preloaddirective to the HSTS header. That's it. It doesn't contact anyone or register your domain anywhere. The directive is just a signal that you intend to submit.
To actually get on the list, you go to hstspreload.org, enter your domain, and submit it manually. The maintainers (Google) check that your site is correctly configured - returning the HSTS header with a long enough max-age, with includeSubDomains, with preload - and if it passes, add your domain. That list is then compiled into future browser releases. You can check whether your domain is on it at hstspreload.org.
It is a permanent commitment. Turning SECURE_HSTS_PRELOAD back to Falsejust stops sending the directive - it does not remove you from the list. Removal requires a manual request at hstspreload.org, and then months for the change to propagate through browser releases. In the meantime, any subdomain that doesn't serve HTTPS will be broken for all users. Don't add preload until you are certain every subdomain serves HTTPS and always will.
Django settings
# settings.py
# Redirect all HTTP → HTTPS at the Django level
# If you're behind a load balancer or reverse proxy (nginx, AWS ALB etc.)
# that terminates TLS, do the redirect there instead and leave this False -
# Django never sees plain HTTP because the proxy handles it first.
SECURE_SSL_REDIRECT = True
# If behind a proxy, tell Django the original request was secure:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
# Send the HSTS header
SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True # adds the preload directive - still need to submit manually