Redirecting HTTP to HTTPS isn’t quite enough. The very first request a browser makes, when someone types example.com or follows an old http:// link, still goes out unencrypted, and an attacker on the network can intercept it before the redirect happens (“SSL stripping”). HSTS (HTTP Strict Transport Security) tells browsers: for this domain, only ever use HTTPS. After one visit, the browser upgrades every request itself.

How it works

Your site sends a response header over HTTPS:

Strict-Transport-Security: max-age=31536000; includeSubDomains

The browser remembers it for max-age seconds, here one year. During that time:

  • every http://example.com link or typed URL is internally rewritten to https:// before any network request;
  • certificate errors become non-bypassable. The user can’t click “proceed anyway”.

Browsers ignore the header when it’s sent over plain HTTP.

Directives

Directive Meaning
max-age=N How long (seconds) to remember. Required
includeSubDomains Apply to every subdomain as well
preload Signals consent to be included in browsers’ built-in HSTS preload list

Add HSTS in Nginx

In the HTTPS server block (listen 443):

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
  • always sends it on error responses too.
  • Nginx’s add_header isn’t inherited into a location block that has its own add_header directives. Repeat it there, or use an include file. See Nginx security headers.

Keep the HTTP → HTTPS redirect for first-time visitors:

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

Add HSTS in Apache

<VirtualHost *:443>
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
    …
</VirtualHost>

Enable mod_headers first: sudo a2enmod headers && sudo systemctl reload apache2.

Verify

curl -sI https://example.com | grep -i strict-transport-security

In Chrome, chrome://net-internals/#hsts lets you query, and for testing delete, a domain’s HSTS state.

The risks: why to roll out gradually

HSTS is a commitment the browser enforces, and you can’t take it back quickly:

  • If your certificate expires or breaks, visitors can’t bypass the error, and the site is unreachable until it’s fixed.
  • With includeSubDomains, every subdomain must support HTTPS: mail., dev., an old intranet. that only speaks HTTP, an internal tool. They all become unreachable over HTTP.
  • Removing HSTS means sending max-age=0, but browsers that cached the old policy only learn this when they visit again over HTTPS.

Safe rollout

  1. Make sure automatic certificate renewal works and is monitored. See Let’s Encrypt with Certbot and SSL expiry checks.
  2. Start small, without subdomains:
Strict-Transport-Security: max-age=300
  1. Check everything works, then increase step by step: 1 day (86400), 1 week, 1 month.
  2. Inventory all subdomains, make sure each has HTTPS, then add includeSubDomains.
  3. Settle at one year (31536000) or two years (63072000).

Preloading

Even with HSTS, the very first visit from a new browser is unprotected. The HSTS preload list is built into Chrome, Firefox, Safari and Edge, and protects listed domains from the first request.

Requirements, via hstspreload.org:

  • a valid certificate, and HTTP redirecting to HTTPS on the same host;
  • all subdomains served over HTTPS;
  • the header on the base domain: max-age of at least one year, plus includeSubDomains and preload.
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

Preloading is very hard to undo. Removal takes months to reach users’ browsers. Only preload domains where HTTPS everywhere is permanent. Some TLDs (like .dev and .app) are preloaded entirely.

Common questions

Do I still need the HTTP→HTTPS redirect? Yes, for first-time visitors and non-browser clients.

Does HSTS affect SEO? Not directly. HTTPS is a ranking signal, and HSTS just makes HTTPS consistent and slightly faster by skipping the redirect for returning visitors.

Behind Cloudflare or a CDN? You can set HSTS at the CDN (Cloudflare has an HSTS setting) or at the origin. Make sure it isn’t sent twice with different values.

Quick reference

Task Setting
Nginx add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Apache Header always set Strict-Transport-Security "…"
Test curl -sI https://site | grep -i strict
Start safely max-age=300, then increase
Remove max-age=0 (served over HTTPS)

Related: TLS configuration and SSL/TLS explained.

  • SSL
  • Security
  • Nginx