HTTP Strict Transport Security looks simple: send a header, force HTTPS, move on.

Until somebody sets it too aggressively.

Then you get broken subdomains, painful rollback, local dev weirdness, certificate fires, and a support thread full of “the site is down for me” even though your origin is technically healthy.

I like HSTS. You should probably use it. But I’ve also seen teams turn a good control into a self-inflicted outage because they treated it like a checkbox instead of a deployment with blast radius.

What HSTS actually does

The browser receives a response header like this:

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

That tells the browser:

  • only use HTTPS for this host
  • remember that rule for max-age seconds
  • if includeSubDomains is present, apply it to every subdomain too

If preload is added and the domain is accepted into browser preload lists, the browser can enforce HTTPS before the first request ever happens.

Basic example:

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

Aggressive? Very.

Potentially correct? Also yes.

Safe to deploy casually? Absolutely not.

The failure mode people underestimate

HSTS is a client-side memory. Once a browser caches it, your server does not get to negotiate anymore.

If the user later tries http://example.com, the browser upgrades it to https://example.com before sending the request.

That means if HTTPS is broken, expired, misrouted, or missing on some host covered by HSTS, the user often cannot click through. The browser hard-fails.

That’s the point of HSTS. It also means your mistakes get enforced by the client.

What “too aggressive” usually means

In practice, aggressive HSTS usually means one or more of these:

  • very long max-age too early
  • includeSubDomains before every subdomain is HTTPS-ready
  • preload before the org can actually support preload requirements permanently
  • applying HSTS on environments that should never have had it
  • shipping it during a migration when DNS, certs, and redirects are still moving

Each of these can hurt differently.

Problem 1: Long max-age makes rollback slow and ugly

If you start here on day one:

Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

you’re telling browsers to remember this policy for two years.

If you later discover a broken subdomain or a certificate issue, changing your mind is not instant. Users who already cached the old header keep enforcing it until the cached policy expires, unless they receive a new header over a valid HTTPS connection that lowers or clears it.

That last part matters: if HTTPS is broken, you may not be able to send the fix.

Rollback header:

Strict-Transport-Security: max-age=0

That removes HSTS, but only for clients that can still successfully connect over HTTPS and receive it.

If the problem is “the cert is dead and browsers refuse to connect,” your rollback is trapped behind the outage.

My rule: start short, then extend.

Good rollout pattern:

Strict-Transport-Security: max-age=300

Then:

Strict-Transport-Security: max-age=86400

Then:

Strict-Transport-Security: max-age=2592000

Then eventually:

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

Don’t jump straight to one year unless you’ve already done the work.

Problem 2: includeSubDomains breaks forgotten hosts

This is the classic outage.

You secure www.example.com and example.com, then enable:

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

But somewhere in the organization, these still exist:

  • old-admin.example.com
  • status.example.com
  • mta-sts.example.com
  • legacy-api.example.com
  • dev.example.com

Maybe one has no TLS. Maybe one has a self-signed cert. Maybe one points to an abandoned load balancer. Maybe it only exists for internal users who now can’t reach it from managed browsers.

Once a browser sees includeSubDomains, all of those become HTTPS-only too.

That includes hosts your team forgot existed.

Before enabling includeSubDomains, inventory subdomains like you mean it. DNS zones, wildcard records, cloud load balancers, old SaaS integrations, internal docs, everything.

If you want a quick sanity check on current headers, run a scan with HeaderTest.

Problem 3: preload is basically a one-way door

Preload is where teams get overconfident.

A typical preload-ready header looks like this:

Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

Browser preload lists require strict conditions, and once your domain is baked into those lists, rollback is slow. Removal requests exist, but browser release cycles are not your incident response plan.

If your company has any chance of needing plain HTTP on any subdomain later, or if you don’t fully control the namespace, don’t preload yet.

Preload means:

  • every subdomain must be HTTPS-capable
  • this needs to stay true long term
  • emergency exceptions are painful
  • mergers, acquisitions, vendor cutovers, and shadow IT become your problem

I’m opinionated here: preload is for mature domains with disciplined DNS ownership, certificate automation, and no mystery hosts. Everybody else should wait.

Official docs worth reading before doing this:

Problem 4: local and staging environments get weird

You generally do not want HSTS on random test domains, temporary hosts, or local environments.

Bad example:

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

served from:

  • staging.example.com
  • qa.example.com
  • dev.example.com

Now every browser that hits those may enforce HTTPS for a year. If your staging cert setup changes, or if someone expects plain HTTP for debugging, things get annoying fast.

Worse, if developers use real subdomains under the production parent domain, includeSubDomains from production can affect them too.

Safer pattern:

  • production domain gets HSTS
  • isolated staging domains use separate parent domains if possible
  • local development uses names outside the production namespace

If you must test HSTS behavior, use short max-age:

Strict-Transport-Security: max-age=60

Not one year. Not preload.

Problem 5: certificate mistakes become full outages

Without HSTS, some browsers let users click through certificate warnings. That’s not ideal, but it exists.

With HSTS, certificate errors usually become hard stops.

That means aggressive HSTS raises the operational bar for:

  • certificate renewal
  • intermediate chain correctness
  • SAN coverage
  • wildcard assumptions
  • load balancer consistency
  • CDN origin TLS config

A missed renewal on a regular HTTPS site is already bad. A missed renewal on an HSTS-protected site is “users cannot bypass this” bad.

That’s why I only like long HSTS durations when certificate automation is boring and proven.

Safe deployment patterns

Nginx

Start conservative:

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

Later:

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

Eventually, if you genuinely qualify:

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

Apache

Header always set Strict-Transport-Security "max-age=300"

Then:

Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"

Express.js

app.use((req, res, next) => {
  res.setHeader("Strict-Transport-Security", "max-age=300");
  next();
});

Or with Helmet:

import helmet from "helmet";

app.use(
  helmet.hsts({
    maxAge: 300,
    includeSubDomains: false,
    preload: false
  })
);

Then increase after validation.

How to back out if you messed up

If HSTS was set too aggressively and HTTPS is still working, send:

Strict-Transport-Security: max-age=0

If you used includeSubDomains, make sure the fix is served from the affected hostnames where possible.

If you used preload, removing the header is not enough for preloaded browsers. You’ll need to follow the browser preload removal process and wait for releases to propagate.

That delay is exactly why preload should be treated like a long-term commitment, not a quick hardening win.

A practical rollout checklist

Before enabling aggressive HSTS, I’d want yes answers to these:

  • Do all public subdomains support valid HTTPS?
  • Do we fully control the DNS namespace?
  • Are cert renewals automated and monitored?
  • Can we test every edge, CDN, and origin path?
  • Are staging and dev isolated from production subdomains?
  • Have we started with a short max-age first?
  • Do we have an inventory of old and “unused” subdomains?
  • Are we ready for certificate errors to become non-bypassable?

If any of those are shaky, don’t go aggressive yet.

My default recommendation

For most teams, this is the sane target:

Strict-Transport-Security: max-age=31536000

Then after you’ve verified subdomain hygiene:

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

And only after real operational confidence:

Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

HSTS is one of those controls where caution is not weakness. The secure setting is only good if you can live with it on your worst day, not just your best day.