HSTS Header Explained: How to Force HTTPS on Your Domain
HSTS (HTTP Strict Transport Security) tells browsers to always use HTTPS for your domain. Learn how it works, how to enable it, what preloading means, and common pitfalls.
HSTS (HTTP Strict Transport Security) is a single HTTP header that tells browsers: "Once you've seen my HTTPS site, always use HTTPS for this domain — even if the URL says HTTP." It prevents a class of attacks where an attacker downgrades your HTTPS connection to HTTP before the browser has a chance to be redirected.
One header, correctly configured, eliminates SSL-stripping attacks entirely.
The Attack HSTS Prevents
Without HSTS, here's what a man-in-the-middle attack looks like on an HTTP connection:
- User types
yourbank.comin browser (no HTTPS) - Browser sends HTTP request over the network
- Attacker intercepts the HTTP request before it reaches your server
- Attacker proxies the request to your HTTPS server, receiving the legitimate HTTPS response
- Attacker sends an HTTP response to the browser with the sensitive content
- Browser shows your bank's page — but over HTTP, fully visible to the attacker
Your server sends a 301 Redirect to HTTPS — but the attacker strips it. The browser never makes an HTTPS connection.
HSTS solution: After the first HTTPS visit, the browser refuses to make HTTP connections to your domain at all. The attacker can't intercept an HTTP request that never happens.
HSTS Header Syntax
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
max-age
How long (in seconds) the browser should remember to use HTTPS only.
| Duration | Seconds |
|---|---|
| 1 week | 604800 |
| 1 month | 2592000 |
| 6 months | 15768000 |
| 1 year | 31536000 |
| 2 years | 63072000 |
Start with a short value (1 week) during testing. Once you're confident everything works, increase to 1–2 years.
includeSubDomains
Applies HSTS to all subdomains. If yourapp.yourdomain.com doesn't have HTTPS, users will get an error after visiting the main domain with HSTS.
Only add this after verifying every subdomain serves HTTPS.
preload
Flags your domain for inclusion in browser preload lists. Preloaded domains are hardcoded as HTTPS-only in Chrome, Firefox, Safari, and Edge — so even on first visit, before seeing any HSTS header, the browser uses HTTPS.
Requirements for preload:
max-age≥ 31536000 (1 year)includeSubDomainsmust be presentpreloaddirective must be present- Submit at hstspreload.org
Warning: Once on the preload list, removal takes months and requires all subdomains to be removed from browsers' hardcoded lists. Only preload if you're certain you want HTTPS-only permanently.
How to Enable HSTS
nginx
server {
listen 443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
# Start with a short max-age for testing
add_header Strict-Transport-Security "max-age=604800" always;
# After testing, switch to:
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}
The always parameter ensures the header is sent even on error responses (4xx, 5xx).
Apache
<VirtualHost *:443>
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
</VirtualHost>
Requires mod_headers to be enabled.
Next.js (next.config.ts)
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains',
},
],
},
];
},
};
Caddy
Caddy enables HSTS automatically when you configure HTTPS. To customize:
yourdomain.com {
header Strict-Transport-Security "max-age=31536000; includeSubDomains"
}
Common HSTS Pitfalls
1. Setting HSTS on HTTP (does nothing)
HSTS headers are only processed by browsers over HTTPS. Sending the header over HTTP is ignored. Make sure your HTTPS server sends the header, not your HTTP redirect server.
2. Adding includeSubDomains too early
If you enable HSTS with includeSubDomains but have subdomains serving only HTTP (dev environments, staging, internal tools), all those subdomains will fail for users who've visited the main domain.
Audit every subdomain before adding includeSubDomains.
3. Short max-age provides weak protection
A max-age of 300 seconds means HSTS expires 5 minutes after the last visit. Attackers who can watch traffic patterns can time attacks during the gap. Use at least 1 month; prefer 1 year.
4. Locked out of your own site
If you set a long max-age and then let your SSL certificate expire (or misconfigure it), users with HSTS cached will get an error page with no way to proceed. There's no clicking through an HSTS error like there is for expired certificates.
Fix: Always renew SSL before expiry. Test your renewal process.
5. Development environment conflicts
If your development environment serves http://localhost, HSTS from production won't affect it (HSTS doesn't apply to localhost). But if you develop using a custom .local or .dev hostname that matches production subdomains with includeSubDomains, you may hit issues.
Solution: Use distinct hostnames for development, or use a local SSL setup.
HSTS and HTTP Redirects
HSTS does not replace your HTTP → HTTPS redirect. You still need the redirect for first-time visitors (before HSTS is cached). The redirect ensures users are sent to HTTPS; HSTS ensures they stay there.
# HTTP redirect (required)
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
return 301 https://yourdomain.com$request_uri;
}
# HTTPS server (sends HSTS header)
server {
listen 443 ssl;
server_name yourdomain.com www.yourdomain.com;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}
Verifying HSTS
Check that the header is being sent:
curl -sI https://yourdomain.com | grep -i strict-transport
Expected output:
strict-transport-security: max-age=31536000; includeSubDomains
Check your preload status at hstspreload.org/status?domain=yourdomain.com.