The HSTS header is a single HTTP response header, Strict-Transport-Security, that tells a browser to only ever connect to your site over HTTPS and to refuse plain HTTP for a set period of time. It exists to close a specific gap: even if every page on your site redirects HTTP to HTTPS, the very first request a user types (example.com, not https://example.com) still goes out in cleartext, and that one request is enough for an attacker on the same network to intercept and downgrade the connection. HSTS removes that window by making the browser itself rewrite http:// to https:// before a single byte leaves the machine.
The mechanism was standardized in RFC 6797 back in 2012 and is supported by every current browser. Despite that, a missing or misconfigured HSTS header remains one of the most common findings in web application security reports, and it is cheap to fix once you understand the moving parts.
What does the Strict-Transport-Security header actually do?
When a browser receives a response containing the header over an HTTPS connection, it records a policy for that hostname. On every subsequent request within the policy window, the browser upgrades any http:// link, bookmark, or typed address to https:// internally, and it will not let the user click through a certificate warning to reach the site. That last part matters — without HSTS, a user can accept an invalid certificate and continue; with HSTS, the connection simply fails, which is the correct behavior when something is tampering with the transport layer.
A typical header looks like this:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Three directives are in play. max-age is the lifetime of the policy in seconds (31536000 is one year). includeSubDomains extends the rule to every subdomain. preload signals your intent to be baked into browsers' hardcoded HSTS lists.
How do you set the max-age without locking yourself out?
The max-age directive is where teams get burned, because the policy is sticky. Once a browser has cached a one-year policy, you cannot revoke it by removing the header — the browser keeps enforcing HTTPS-only until the timer expires or you explicitly ship max-age=0.
The safe rollout is to start small and ramp up. Begin with something like max-age=300 (five minutes) so you can confirm every subdomain, redirect, and internal tool genuinely serves valid HTTPS. Once you have watched traffic for a few days with no broken clients, raise it to a day, then a week, then the recommended one year. If you jump straight to a year and discover an internal admin panel that only listens on HTTP, you have locked every browser that visited out of that panel for twelve months.
includeSubDomains deserves the same caution. It applies the policy to *.example.com, so a legacy blog.example.com or status.example.com that is not HTTPS-ready will break for anyone who has visited the parent domain. Inventory your subdomains before you add the directive, not after.
What is HSTS preload and should you use it?
Preload closes the remaining first-visit gap. Even with HSTS, the very first time a browser ever contacts your domain it has no cached policy, so that initial request can still be intercepted. The preload list — maintained at hstspreload.org and shipped inside Chrome, Firefox, Safari, and Edge — hardcodes your domain as HTTPS-only from the moment the browser is installed.
To qualify you must serve max-age of at least 31536000, include both includeSubDomains and preload, and redirect HTTP to HTTPS on the same host. Understand the commitment before you submit: getting off the preload list can take months to propagate through browser releases, so treat preload as a decision you make once your entire domain tree is unambiguously HTTPS-only and expected to stay that way.
How do you configure HSTS on common servers?
On nginx, add the header inside your TLS server block so it is only sent over HTTPS:
server {
listen 443 ssl;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}
The always flag ensures the header is emitted even on error responses. On Apache with mod_headers:
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
One rule holds across every platform: only send HSTS over HTTPS. The spec says browsers must ignore the header when it arrives over plain HTTP, because a network attacker could otherwise inject a bogus policy. Send it exclusively from your TLS listener, and keep a clean HTTP-to-HTTPS redirect in front of everything.
Why does a missing HSTS header keep showing up in scans?
Because it is a low-effort, high-signal finding, nearly every scanner reports it. A missing HSTS header in Checkmarx, for example, surfaces as a configuration weakness with a clear remediation path, and the same finding appears in ZAP, Burp Suite, and generic security-header graders. These tools cannot see your redirect logic; they simply observe that the response lacks Strict-Transport-Security and flag the downgrade risk.
The finding is legitimate but low-severity on its own — it is a hardening gap, not an active exploit. The trap is treating a scanner list as a priority list. A missing HSTS header on a marketing page matters far less than a missing one on the domain that hosts your authentication flow. Understanding why a control exists is what lets you rank findings sensibly, which is a core theme of practical application security work. If you run dynamic scans as part of CI, a tool such as Safeguard's DAST engine will report the same header gaps alongside more serious runtime issues, so triaging by real exposure rather than raw count keeps the queue honest.
What are the common HSTS mistakes to avoid?
The recurring failures are predictable. Setting a long max-age before every subdomain is HTTPS-ready locks users out. Adding includeSubDomains without an inventory breaks legacy hosts. Submitting to preload before you are certain the whole tree stays HTTPS-only creates a slow, painful reversal. Sending the header over HTTP means browsers ignore it, so the control silently does nothing. And forgetting the always flag on nginx means error pages ship without the header, giving scanners a valid reason to keep flagging you.
Get those five right and HSTS becomes a set-and-forget control that meaningfully raises the cost of a transport-layer attack.
FAQ
Is the HSTS header enough to secure my site over HTTPS?
No. HSTS only guarantees that the transport uses HTTPS; it does nothing about application-layer flaws, weak TLS ciphers, or an expired certificate. Treat it as one layer alongside a valid certificate chain, modern TLS configuration, and the rest of your security headers.
What happens if I remove the HSTS header after deploying it?
Browsers keep enforcing the last policy they cached until its max-age expires. To actively cancel it, serve Strict-Transport-Security: max-age=0 and let clients pick it up before you stop sending the header entirely.
Does HSTS protect the very first visit to my site?
Not by itself. The first-ever request to a domain with no cached policy can still be intercepted. HSTS preload closes that gap by hardcoding your domain into the browser before any visit happens.
Why does Checkmarx report a missing HSTS header even though I redirect to HTTPS?
Scanners inspect the response headers directly and cannot infer your redirect behavior. A missing HSTS header in Checkmarx or any similar tool means the Strict-Transport-Security header was absent from the response, so the browser downgrade window it protects against is still technically open.