What is HSTS, and why does missing it leave a gap on the first visit?
A visitor types yoursite.com into the address bar and hits enter. No scheme, just the bare domain. Before your 301 ever gets a chance to push them to HTTPS, the browser has to guess which protocol to open with. What does it pick?
It picks http://, because that's what a bare domain defaults to and nobody typed otherwise. So the first request leaves the machine in plaintext, redirect or not. Your redirect to HTTPS is real and it works, but it can only fire after that first plaintext request has already crossed the network. The window you can't cover with a redirect is the one before it.
The window your redirect can't cover
A 301 from HTTP to HTTPS works by sending a plaintext response that says "go to the secure version." That response is the problem. It travels over the same unencrypted connection as the request, across whatever network the visitor is on. An attacker sitting on that network doesn't have to break HTTPS. They just have to catch the plaintext request that comes before it and answer differently.
This is an SSL-stripping attack, and the tools for it are old and reliable. The attacker intercepts the plaintext request, suppresses the redirect, and proxies the connection: HTTPS to your server on one side, plaintext to the victim on the other. The visitor never sees a warning because their browser never got told to expect HTTPS. They type their password into a page that looks exactly like yours, on a connection the attacker reads end to end.
What the header actually does
Strict-Transport-Security closes the timing gap by moving the decision off the network and into the browser. The first time a visitor reaches you over HTTPS, you send a header like this:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
From that moment, for max-age seconds, the browser refuses to make a plaintext request to your domain at all. It rewrites http://yoursite.com to https:// internally, before anything leaves the machine. There's no plaintext request for an attacker to catch, so there's nothing to strip. includeSubDomains extends that rule to every subdomain. preload, paired with submitting your domain to the browser preload list, closes even the very first visit, because the rule ships inside the browser itself and applies before the user has ever been to your site.
The catch is the honest part: HSTS only protects a browser that has already heard from you once, unless you're on the preload list. The header is a promise the browser remembers. The first uncached visit, on a fresh browser, still goes out the door the way the user typed it. That's why preload matters, and why you want both HSTS and a clean redirect in place. Each covers a gap the other leaves open.
How to check
You can read your own headers in a few seconds.
- From a terminal:
curl -sI https://yoursite.com | grep -i strict-transport. If nothing comes back, the header isn't being sent. - If it is present, read the
max-age. A value of a few hundred seconds is barely a header at all. You want a year,31536000. - Check whether
includeSubDomainsis there. A subdomain without protection is a door into the cookie scope you thought you'd locked.
That curl line is exactly what SurfaceCheckr does for you, from outside, the same first secure response a visitor's browser would receive. We read the Strict-Transport-Security line your server actually sends, report whether it's there, how long the max-age runs, and whether subdomains are covered. We don't log into anything or test your code; we just read the public promise your server makes about future requests, and tell you if it's missing.
The fix
One header, set at your edge or server, on every HTTPS response.
# nginx: HTTPS works, but the browser is
# told nothing about future requests
server {
listen 443 ssl;
server_name yoursite.com;
# ...no Strict-Transport-Security header
}Set max-age short the first time if you're nervous, confirm everything still loads, then raise it to a year. Add preload and submit the domain only once you're sure every subdomain will serve HTTPS indefinitely, because backing out of preload is slow.
This only earns its keep if your HTTP requests already redirect to HTTPS and your TLS certificate isn't about to expire. Send the header, then run curl -sI against your own domain and read the line back. If it isn't there, every first visit is the one an attacker waits for.
Find it before someone else does.
Paste your domain. The grade and issue count are free, and you'll see in a couple of minutes exactly what's reachable from outside.