HSTS is one of those controls people configure once, feel good about, and then forget for two years.

That is exactly why it needs monitoring.

I’ve seen teams proudly preload a domain, then quietly lose the header on a CDN edge, a redirect hop, or a newly launched subdomain. Nobody notices until someone runs a scan, a browser behavior changes, or a security review turns up a gap that has been sitting there for months.

The tricky part is that HSTS monitoring is easy to do badly. You can absolutely build something that says “header exists” and still miss the real failures.

Here are the mistakes I see most often, and how I’d fix them.

1. Only checking the homepage

A lot of teams monitor https://example.com/ and call it done.

That catches almost nothing.

HSTS behavior depends on how users and bots actually hit your site:

  • apex domain: https://example.com
  • www: https://www.example.com
  • login or account subdomains
  • region-specific hosts
  • API domains
  • redirect entry points from http://

The most common blind spot is checking only the final HTTPS page while completely ignoring the redirect chain. If your HTTP listener is broken, or your CDN config differs between hosts, your monitor can stay green while users still take unsafe or inconsistent paths.

Fix

Monitor all real entry points, not just one URL.

At minimum, check:

  • http://example.com
  • http://www.example.com
  • https://example.com
  • https://www.example.com

If you use includeSubDomains, add every important subdomain to monitoring anyway. Policy inheritance is nice in theory, but in real environments subdomains get moved, delegated, or fronted by different infrastructure.

A basic shell check looks like this:

#!/usr/bin/env bash
set -euo pipefail

URLS=(
  "https://example.com"
  "https://www.example.com"
  "https://login.example.com"
  "https://api.example.com"
)

for url in "${URLS[@]}"; do
  echo "Checking $url"
  curl -sI "$url" | awk 'BEGIN{IGNORECASE=1} /strict-transport-security/ {print}'
done

That is still primitive, but it is already better than “we check the homepage once a day.”

2. Alerting only when the header disappears

This is probably the biggest monitoring mistake.

If your alert fires only when Strict-Transport-Security is completely missing, you will miss dangerous regressions like:

  • max-age dropping from 31536000 to 300
  • includeSubDomains disappearing
  • preload disappearing
  • syntax bugs that make the header invalid
  • inconsistent values across nodes or regions

A weaker HSTS policy is still a failure.

Fix

Parse the header and validate each directive against your expected policy.

For example, if your target policy is:

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

then your monitor should explicitly check:

  • header exists
  • max-age >= 31536000
  • includeSubDomains present
  • preload present if you rely on preload eligibility

Here’s a simple Node.js example:

import https from "node:https";

const expected = {
  minMaxAge: 31536000,
  includeSubDomains: true,
  preload: true,
};

function parseHsts(header) {
  if (!header) return null;

  const parts = header.split(";").map(p => p.trim());
  const result = {
    maxAge: null,
    includeSubDomains: false,
    preload: false,
  };

  for (const part of parts) {
    const lower = part.toLowerCase();
    if (lower.startsWith("max-age=")) {
      result.maxAge = parseInt(part.split("=")[1], 10);
    } else if (lower === "includesubdomains") {
      result.includeSubDomains = true;
    } else if (lower === "preload") {
      result.preload = true;
    }
  }

  return result;
}

function checkHost(host) {
  return new Promise((resolve, reject) => {
    const req = https.request(
      { host, method: "HEAD", path: "/" },
      res => {
        const raw = res.headers["strict-transport-security"];
        const parsed = parseHsts(raw);

        if (!parsed) {
          return reject(new Error(`${host}: HSTS header missing`));
        }

        if (!parsed.maxAge || parsed.maxAge < expected.minMaxAge) {
          return reject(new Error(`${host}: max-age too low: ${parsed.maxAge}`));
        }

        if (expected.includeSubDomains && !parsed.includeSubDomains) {
          return reject(new Error(`${host}: includeSubDomains missing`));
        }

        if (expected.preload && !parsed.preload) {
          return reject(new Error(`${host}: preload missing`));
        }

        resolve(`${host}: OK`);
      }
    );

    req.on("error", reject);
    req.end();
  });
}

checkHost("example.com")
  .then(console.log)
  .catch(err => {
    console.error(err.message);
    process.exit(1);
  });

That gets you much closer to useful alerting.

3. Ignoring redirect behavior

HSTS and redirects are joined at the hip. Most users first hit http:// from old links, bookmarks, or typed URLs. If your redirect chain is messy, your monitoring should catch it.

I’ve seen setups like this:

  • http://example.comhttps://www.example.com
  • http://www.example.comhttps://www.example.com
  • https://example.com serves one HSTS policy
  • https://www.example.com serves a different one
  • one edge location forgets the header on 301 responses

That kind of inconsistency causes weird behavior and hard-to-debug reports.

Fix

Monitor the entire chain, not just the final response. Record:

  • each hop
  • status code
  • location header
  • HSTS header on HTTPS responses
  • final canonical destination

A quick curl command helps during debugging:

curl -s -I -L -o /dev/null -D - http://example.com

For production monitoring, store redirect-chain metadata so you can compare changes over time. If a redirect target changes unexpectedly, that should be alertable too.

4. Treating preload like a checkbox

Teams love saying “we’re preloaded” as if that means the work is finished.

Preload introduces another monitoring problem: your live header and your preload expectations can drift apart. If preload or includeSubDomains disappears later, browsers with the preload list still behave one way, while your actual config says something else. That mismatch can bite you during migrations and subdomain changes.

Fix

If you rely on preload, monitor both:

  1. live header compliance
  2. preload list presence for the domain

You should alert if a preloaded domain stops serving a preload-eligible header, because that usually means someone changed config without understanding the consequences.

If you want a quick external check while debugging header issues, run a free security headers scan. I like having an outside view because internal checks sometimes miss CDN or edge-specific behavior.

5. No regional or edge visibility

Your monitor from one US region says HSTS is fine. Great. Meanwhile an EU edge node is missing the header after a partial rollout.

This happens all the time with CDNs, multi-region load balancers, and canary releases.

Fix

Run checks from multiple regions or at least from your major traffic footprints. If you can’t do full global synthetic monitoring, do the next best thing:

  • one internal check from your infra
  • one external check from a third-party uptime platform
  • one periodic scan from a different region or cloud provider

The goal is not perfection. The goal is to catch “works from here” nonsense before customers do.

6. Alerting too often and training everyone to ignore it

Bad security alerting usually fails in one of two ways:

  • no alerts at all
  • constant alerts everyone mutes

HSTS monitoring gets noisy when people alert on every transient timeout, every single endpoint, or every repeated failure from the same root cause.

Fix

Alert on state change and sustained failure.

A good pattern is:

  • warning after 2 failed checks over 5 minutes
  • critical after 10–15 minutes of continuous failure
  • recovery notification when fixed
  • deduplication by domain and root cause

Also separate availability failures from policy failures. “Site timed out” is not the same as “HSTS max-age dropped below baseline.” Route them differently if needed.

A simple rules model looks like this:

alerts:
  - name: hsts_missing
    condition: header_missing_for_3_checks
    severity: critical

  - name: hsts_policy_regression
    condition: max_age_below_baseline OR includesubdomains_missing
    severity: high

  - name: endpoint_unreachable
    condition: timeout_for_3_checks
    severity: medium

That structure makes triage much less painful.

7. Not keeping a known-good baseline

If your monitoring logic says “header exists,” but nobody knows the intended policy, you are not monitoring. You are just collecting strings.

Teams change CDNs, reverse proxies, and app frameworks. Headers get rewritten. Without a baseline, nobody can tell whether a new value is acceptable.

Fix

Define the expected HSTS policy in code and keep it near your infrastructure config.

For example:

hsts:
  expected:
    max_age: 31536000
    include_subdomains: true
    preload: true
  hosts:
    - example.com
    - www.example.com
    - login.example.com
    - api.example.com

Then use the same source of truth for:

  • deployment validation
  • synthetic monitoring
  • CI checks
  • incident response docs

If someone intentionally changes HSTS, they should update the baseline in the same pull request.

8. Forgetting to monitor new subdomains

This one is painfully common in larger orgs.

A team launches billing.example.com or status.example.com through a different provider. It never gets added to monitoring. Months later, someone discovers it has no HSTS header, or worse, it is HTTP-accessible and outside your expected policy.

Fix

Tie HSTS monitoring to your asset inventory.

At minimum:

  • pull hostnames from DNS, ingress configs, or service catalogs
  • compare discovered hosts against monitored hosts
  • alert on unmanaged internet-facing subdomains

Security controls fail at the edges of ownership. If nobody owns the hostname list, your HSTS coverage will rot.

9. Not testing after deployments

Teams often rely on periodic monitoring every hour or every day. That is way too slow for a config that can break instantly after a CDN, Nginx, or load balancer change.

Fix

Run HSTS validation as part of deployment and post-deploy smoke tests.

For example, after a production rollout:

EXPECTED="max-age=31536000; includeSubDomains; preload"
ACTUAL=$(curl -sI https://example.com | awk 'BEGIN{IGNORECASE=1} /strict-transport-security/ {sub(/\r/,""); print $0}')

echo "$ACTUAL"

if [[ "$ACTUAL" != *"max-age=31536000"* ]] || [[ "$ACTUAL" != *"includeSubDomains"* ]]; then
  echo "HSTS validation failed"
  exit 1
fi

This catches the obvious breakage before your scheduled monitor does.

What I’d actually run

If I were setting this up for a real production site, I’d use a layered approach:

  • CI check for expected HSTS config
  • post-deploy smoke test against live hosts
  • synthetic monitoring from multiple regions
  • alerting on missing header and policy regression
  • inventory-based checks for new subdomains
  • periodic external validation

That sounds like overkill until the day a proxy config change strips headers from one hostname and your preload assumptions stop matching reality.

HSTS is simple on paper. Operationally, it is one of those controls that quietly decays unless you watch it like a hawk. The fix is not fancy tooling. It is being explicit about what “good” looks like, checking the real paths users hit, and refusing to accept a green dashboard built on shallow checks.