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.comhttp://www.example.comhttps://example.comhttps://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-agedropping from31536000to300includeSubDomainsdisappearingpreloaddisappearing- 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 >= 31536000includeSubDomainspresentpreloadpresent 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.com→https://www.example.comhttp://www.example.com→https://www.example.comhttps://example.comserves one HSTS policyhttps://www.example.comserves 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:
- live header compliance
- 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.