How to Set Up Nginx with HSTS (HTTP Strict Transport Security)

How to Set Up Nginx with HSTS (HTTP Strict Transport Security)

A client once asked me why their site still occasionally loaded over plain HTTP for some users, even though they had a perfectly good HTTPS redirect in place. The answer, it turned out, was the classic gap in HTTP-to-HTTPS redirection: that very first request. A redirect only kicks in after the browser has already made an initial (unencrypted) connection, which is exactly the window an attacker on the same network can exploit through something like an SSL-stripping attack. HSTS closes that gap, and in this article, I’ll explain exactly what it does, why it matters, and how to configure it correctly in Nginx without locking yourself out of your own domain by mistake — which, trust me, is a very real risk with this particular header if you’re not careful.

What HSTS Actually Does

HTTP Strict Transport Security is a response header that tells a browser, “from now on, always connect to this domain over HTTPS, even if the user types http:// or clicks an old HTTP link.” Once a browser has seen this header from a domain, it will refuse to make a plain HTTP connection to that domain for the duration specified, automatically upgrading any such attempt to HTTPS before a single byte is sent over the insecure connection.

This matters because a standard 301 redirect from HTTP to HTTPS still requires that first HTTP request to happen. An attacker performing a man-in-the-middle attack on an open Wi-Fi network, for instance, could intercept that initial plaintext request and prevent the redirect from ever reaching the browser, serving a fake page instead. HSTS eliminates this window because the browser never sends the HTTP request in the first place — it converts http://example.com to https://example.com internally before making any network call.

Requirements

  • Nginx installed with TLS already configured and working correctly on your domain
  • A valid TLS certificate (Let’s Encrypt via Certbot, or a commercial CA)
  • HTTPS confirmed to be fully functional across your entire site — every page, every asset, every subdomain you intend to cover — before adding HSTS, because once clients start caching the HSTS policy, undoing it is slow and painful

I want to stress that last point. Do not enable HSTS, especially with a long max-age or the includeSubDomains directive, until you’re confident HTTPS works everywhere on your site. If you enable it prematurely and something on your site or a subdomain is broken over HTTPS, users who’ve cached the HSTS policy will be unable to reach that content over HTTP either, and there’s no quick way to reverse it for those users until the max-age expires.

Step 1: Confirm HTTPS Works Correctly First

Before touching HSTS, verify:

curl -I https://example.com
curl -I https://www.example.com

Check every subdomain you use, and don’t forget things like a blog.example.com or shop.example.com that might not have TLS configured yet if you’re planning to use includeSubDomains.

Step 2: Add the HSTS Header to Your Nginx Config

The header is added using add_header within your HTTPS server block:

server {
    listen 443 ssl;
    server_name example.com www.example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    add_header Strict-Transport-Security "max-age=63072000" always;

    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}

Let’s break down the directive:

  • max-age=63072000 — this is the duration, in seconds, that the browser should remember to force HTTPS for this domain. 63072000 seconds is two years, which is the value commonly recommended for HSTS preload eligibility (more on that below). A shorter value like max-age=31536000 (one year) or even max-age=86400 (one day) is a reasonable starting point while you’re still confident-testing the rollout.
  • always — this ensures the header is sent regardless of the response code, including error responses like 404s and 500s, which matters because you want the policy enforced consistently.

Step 3: Ensure HTTP Requests Still Redirect (For First-Time Visitors)

HSTS only works once a browser has seen the header at least once. First-time visitors who type http://example.com still need an HTTP-to-HTTPS redirect for that very first visit:

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

This is why HSTS complements, rather than replaces, your HTTP-to-HTTPS redirect — you need both.

Step 4: Test and Reload

sudo nginx -t
sudo systemctl reload nginx

Verify the header is present:

curl -I https://example.com

You should see:

Strict-Transport-Security: max-age=63072000

in the response headers.

Adding includeSubDomains

If you want the HSTS policy to apply to every subdomain of your domain, not just the exact host, add includeSubDomains:

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;

This is powerful and risky in equal measure. It means blog.example.com, api.example.com, staging.example.com — literally every subdomain — will also be forced to HTTPS by any browser that has seen this header from example.com, regardless of whether those subdomains have HTTPS configured correctly. If you have even one subdomain that doesn’t support HTTPS properly, enabling includeSubDomains will break it for anyone who has visited your main domain.

I recommend only adding this once you’re certain every current and reasonably foreseeable future subdomain either has working HTTPS or simply doesn’t exist yet (and if it doesn’t exist yet, you’re accepting that it’ll need HTTPS from day one when it does).

Adding preload and Submitting to the HSTS Preload List

Browsers maintain a hardcoded list of domains that should always be accessed over HTTPS, even before the very first visit — this eliminates the “first request” gap entirely, since the browser never needs to receive the HSTS header at all for preloaded domains; it already knows.

To be eligible for preloading, your HSTS header needs:

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

Requirements for preload list submission (as of the current policy at hstspreload.org):

  • max-age must be at least 31536000 (one year), though 63072000 (two years) is recommended
  • includeSubDomains must be present
  • preload must be present
  • You must serve a valid HTTPS certificate for the base domain
  • Any HTTP request must redirect to HTTPS

Once your configuration meets these requirements, you submit your domain at hstspreload.org. Be aware this is a serious commitment: removal from the preload list, once your domain is baked into browser source code, can take months to propagate out to users as browsers ship updates. Don’t submit a domain you’re not fully confident about.

Testing Your HSTS Configuration Thoroughly

Beyond the basic curl check, I recommend:

  1. Check header presence on every response type, including error pages: curl -I https://example.com/nonexistent-page The header should still be present on the 404 response, confirming your always flag is working.
  2. Use an online header checker or browser developer tools (Network tab, check response headers) to confirm the exact header value as seen by a real browser.
  3. Test subdomains individually if using includeSubDomains: curl -I https://blog.example.com curl -I https://api.example.com
  4. Use the SSL Labs test (ssllabs.com/ssltest) which specifically reports on HSTS configuration as part of its overall TLS grading.

Troubleshooting Common Issues

Problem: The header isn’t showing up at all.

Check that add_header is inside the correct server block (the HTTPS one, listening on 443) and not accidentally only in the HTTP block, where it would be meaningless. Also confirm there isn’t a duplicate add_header Strict-Transport-Security directive elsewhere in your config being overridden — Nginx only uses add_header directives from the most specific matching block; if a location block has its own add_header directives without repeating the HSTS one, the outer one may get dropped depending on directive inheritance rules. As a rule, if any add_header appears in a location block, you generally need to repeat all headers you want in that block, since add_header directives don’t merge across nested contexts the way you might expect.

Problem: A subdomain broke after enabling includeSubDomains.

This is the classic HSTS mistake. If a subdomain doesn’t support HTTPS and a user has already cached the parent domain’s HSTS policy with includeSubDomains, that subdomain becomes completely unreachable over HTTP for that user until the max-age expires or they manually clear HSTS settings in their browser (which most users won’t know how to do). The fix is to get HTTPS working on that subdomain immediately, since removing the header from the parent domain won’t help users who already cached the old, longer-lived policy.

Problem: I need to “undo” HSTS quickly.

You mostly can’t, for users who’ve already cached the policy, until max-age expires. You can set a very short max-age (like max-age=0) going forward, which will cause browsers to drop the cached policy on their next visit, but this only helps users who visit again during the (now much shorter) remaining validity window of their existing cached policy. This is exactly why testing thoroughly before enabling HSTS — and starting with a short max-age — matters so much.

Problem: Mixed content warnings even with HSTS enabled.

HSTS doesn’t fix mixed content (HTTP resources loaded from an HTTPS page); it only forces the top-level navigation to HTTPS. You still need to fix any hardcoded http:// references to images, scripts, or stylesheets within your pages separately.

Security Considerations

  • Start with a shorter max-age during initial rollout (a day or a week) and only extend it to the full one- or two-year value once you’re confident nothing will break.
  • Don’t add includeSubDomains until every subdomain either has working HTTPS or doesn’t exist. This is worth repeating because it’s the most common way people accidentally break things with HSTS.
  • Preload list submission is close to permanent in practice; don’t rush into it.
  • HSTS protects against downgrade and SSL-stripping attacks but does nothing for certificate validation issues, mixed content, or other TLS misconfigurations — it’s one layer of a broader TLS hardening strategy, not a complete solution on its own.
  • Combine HSTS with other security headers for a more complete posture: add_header X-Content-Type-Options "nosniff" always;add_header X-Frame-Options "SAMEORIGIN" always;add_header Referrer-Policy "strict-origin-when-cross-origin" always;

Performance Tips

HSTS itself has negligible performance overhead — it’s a small header on each response. The real performance benefit, somewhat indirectly, is that once a browser has cached the HSTS policy, it skips the initial HTTP request and redirect round-trip entirely for subsequent visits, connecting directly over HTTPS. That’s a small but real latency improvement, particularly noticeable on mobile networks with higher round-trip times.

Real-World Use Cases

  • E-commerce and banking sites where protecting against SSL-stripping attacks on public Wi-Fi is a serious concern given the sensitivity of the data involved.
  • Login and authentication pages across any site, since credential theft via downgrade attacks is one of the more practical real-world exploits HSTS defends against.
  • Corporate domains submitting to the HSTS preload list as part of a broader security compliance initiative (often driven by requirements like PCI-DSS or internal security audits).
  • API endpoints consumed by mobile apps or other services, where enforcing HTTPS at the protocol level adds a layer of protection beyond what the application code alone provides.

Best Practices Summary

  • Confirm HTTPS works flawlessly across your entire domain and all relevant subdomains before adding HSTS.
  • Start with a short max-age, verify everything works, then increase gradually toward the one- or two-year recommended value.
  • Always pair HSTS with an HTTP-to-HTTPS redirect for first-time visitors.
  • Use always on the add_header directive so the header appears on all response codes.
  • Only add includeSubDomains once every subdomain is HTTPS-ready.
  • Treat preload submission as a serious, hard-to-reverse commitment.
  • Layer HSTS with other security headers rather than treating it as a complete solution on its own.

HSTS Across Multiple Server Blocks and Domains

If you run multiple domains or virtual hosts from the same Nginx instance, remember that HSTS is applied per-domain by the browser, based on the header received from that specific domain — there’s no cross-domain inheritance. Each server block serving a distinct domain needs its own add_header Strict-Transport-Security line if you want HSTS enforced there too:

server {
    listen 443 ssl;
    server_name shop.example.com;
    add_header Strict-Transport-Security "max-age=63072000" always;
    # ...
}

server {
    listen 443 ssl;
    server_name blog.example.net;
    add_header Strict-Transport-Security "max-age=63072000" always;
    # ...
}

If you’re using a shared snippet file to avoid repeating this across many server blocks, that’s a reasonable approach:

# /etc/nginx/snippets/hsts.conf
add_header Strict-Transport-Security "max-age=63072000" always;

Then include it where needed:

server {
    listen 443 ssl;
    server_name shop.example.com;
    include snippets/hsts.conf;
    # ...
}

This keeps your policy consistent across domains without manually duplicating the exact header string everywhere, which reduces the chance of a typo or an inconsistent max-age value creeping in across different parts of your infrastructure.

Frequently Asked Questions

Does HSTS protect against all man-in-the-middle attacks?

No. It specifically protects against SSL-stripping and downgrade attacks that rely on intercepting the initial plaintext HTTP request. It doesn’t protect against a compromised certificate authority, a user who’s been tricked into installing a malicious root certificate, or attacks that don’t involve a protocol downgrade at all. It’s one solid layer among several you need for comprehensive TLS security.

What’s the difference between HSTS and just using a 301 redirect?

A 301 redirect from HTTP to HTTPS still requires the browser to make an initial HTTP request before it can be redirected — and that initial request is exactly the window an attacker can intercept. HSTS, once cached by the browser, prevents that initial HTTP request from ever being sent in the first place for the duration of the policy. They’re complementary, not interchangeable; you need the redirect for genuinely first-time visitors and HSTS for everyone after that.

Can I set different max-age values for different parts of my site?

Not meaningfully — HSTS is a domain-level (or with includeSubDomains, a domain-and-subdomain-level) policy, not a path-level one. If you set the header differently across different location blocks on the same domain, the browser’s actual behavior can become unpredictable since it wasn’t designed with intra-domain path-level distinctions in mind. Keep your HSTS header consistent across your entire domain.

How do I remove my domain from the HSTS preload list if I change my mind?

You submit a removal request through hstspreload.org, but be aware the removal process is significantly slower than addition — it can take multiple browser release cycles (often several months) for the removal to actually propagate to users, since it depends on new browser versions shipping with the updated list. This is exactly why I recommend being very deliberate before submitting in the first place.

Is HSTS relevant if I’m already using a CDN that enforces HTTPS?

Often yes, still worth setting, because HSTS is a browser-level policy independent of your specific infrastructure. Even if your CDN enforces HTTPS at its edge, HSTS ensures the browser itself won’t attempt an insecure connection in the first place, which is a meaningfully different (and earlier) point of protection than anything your CDN or origin server can enforce after a request has already been made.

Wrapping Up

HSTS closes a real and exploitable gap in HTTP-to-HTTPS redirection, and configuring it in Nginx takes all of one line once you understand what each directive actually does. The complexity isn’t in the syntax — it’s in the rollout discipline. Test thoroughly, start conservative, and only ratchet up to the long-lived, subdomain-wide, preloaded configuration once you’re genuinely confident nothing will break. Get that sequencing right, and HSTS becomes a quiet, effective piece of your security posture that you’ll rarely think about again.

Total
1
Shares

Leave a Reply

Previous Post
How to Enable Server Push in Nginx

How to Enable Server Push in Nginx

Next Post
How to Set Up Nginx with Ruby on Rails and Puma

How to Set Up Nginx with Ruby on Rails and Puma

Related Posts