You add Strict-Transport-Security: max-age=31536000; includeSubDomains to your nginx config. Your SSL Labs score jumps to A+. Six weeks later, a CDN rule change silently drops the header. Your score reverts. Nobody notices until a security audit or a bug report.
This guide explains what HSTS does, the most common ways it regresses, how to check it manually, and how to monitor it automatically.
Strict-Transport-Security tells browsers: "Only ever connect to this domain over HTTPS โ even if the user types http://, even if a link says http://." The browser caches this instruction for the duration of max-age (in seconds).
Without HSTS, a user on a coffee shop WiFi who navigates to http://yourdomain.com can be intercepted by an attacker before the redirect to HTTPS happens. The attacker performs an SSL stripping attack โ your server thinks it's talking to the user over HTTPS, but the user's browser is talking to the attacker over HTTP. HSTS prevents this by making the browser refuse to connect over HTTP in the first place.
# A proper HSTS header looks like this:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
The three parts:
max-age=31536000 โ cache this instruction for 1 yearincludeSubDomains โ apply to all subdomains, not just the rootpreload โ eligible for inclusion in browsers' built-in HSTS preload list (submit at hstspreload.org)curl -s -I https://yourdomain.com, not http://.
If you set HSTS in Cloudflare's "Manage Headers" Transform Rules or via the HSTS toggle in SSL/TLS settings, it can be overridden or dropped when you modify other Cloudflare settings. Cloudflare's HSTS setting under SSL/TLS โ Edge Certificates is separate from custom response headers โ having one active doesn't mean both survive a config change.
nginx's add_header directive is not inherited by child location blocks if those blocks define their own add_header. This means adding a single new header in a nested location block can silently remove HSTS from all requests handled by that block:
server {
add_header Strict-Transport-Security "max-age=31536000" always;
location / { try_files $uri $uri/ =404; }
location /api/ {
# Adding this new header here silently removes HSTS from /api/ responses:
add_header X-API-Version "2.0";
proxy_pass http://localhost:3000;
}
}
The fix is to include all security headers in every location block, or to use nginx's headers-more module, or to set headers at a level that isn't affected by child block inheritance.
Moving from Heroku (which strips most security headers) to a VPS, from Apache to nginx, or from a managed hosting provider to a serverless platform resets your headers to whatever the new platform defaults to. New platforms don't import your header config โ you have to add it explicitly.
When SSL termination moves from your application server to a load balancer (AWS ALB, GCP LB, Azure App Gateway), HSTS is often set at the application layer but not re-added at the load balancer layer. The result: HSTS disappears from production responses without any application deployment.
curl -s -I https://yourdomain.com | grep -i strict-transport
# Should output something like:
# strict-transport-security: max-age=31536000; includeSubDomains; preload
If the command produces no output, HSTS is not set. If you see max-age=0, HSTS has been explicitly disabled (this is sometimes done intentionally during migration, but dangerous if unintentional).
The security headers checker shows your full security header grade, including whether HSTS is present, the configured max-age, and whether includeSubDomains and preload are set. It checks all six major security headers in one pass.
Qualys SSL Labs grades HSTS as part of its overall score. An A+ requires HSTS with a max-age of at least 15768000 (6 months) and includeSubDomains. A missing HSTS drops you from A+ to A or lower.
Manual checks work for the deployment you remember to check after. They miss infrastructure changes, CDN config updates, and any regression that happens between deployments.
The simplest automated monitor is a cron job that checks your HSTS header and sends an alert if it's missing:
#!/bin/bash
# /usr/local/bin/check-hsts.sh
DOMAIN="yourdomain.com"
EMAIL="you@yourdomain.com"
HSTS=$(curl -s -I "https://$DOMAIN" | grep -i "strict-transport-security")
if [ -z "$HSTS" ]; then
echo "HSTS MISSING on $DOMAIN" | mail -s "Security alert: HSTS regression on $DOMAIN" "$EMAIL"
fi
# crontab: run every 6 hours
0 */6 * * * /usr/local/bin/check-hsts.sh
This works well if you have a server to run it on. The limitation: it only monitors from one location, it requires maintaining a cron job, and it won't send alerts if your server is the same one that lost HSTS (infrastructure-level monitoring blind spot).
name: HSTS Monitor
on:
schedule:
- cron: '0 8 * * *' # Daily at 8am UTC
jobs:
check-hsts:
runs-on: ubuntu-latest
steps:
- name: Check HSTS header
run: |
HSTS=$(curl -s -I https://yourdomain.com | grep -i strict-transport-security)
if [ -z "$HSTS" ]; then
echo "::error::HSTS header is missing!"
exit 1
fi
echo "HSTS present: $HSTS"
GitHub Actions failed runs send email notifications to the repo owner. This works reasonably well for catching daily regressions, though it only runs once per day and requires a GitHub repo.
The most reliable approach is a service that continuously monitors your security headers from external infrastructure and alerts you the moment something changes โ without requiring you to maintain a cron job or GitHub Action.
HeaderGuard checks your HTTP security headers daily and sends an email alert the moment any header disappears or its value changes โ HSTS, CSP, X-Frame-Options, and more.
Add your domain once. Get notified before a bug bounty hunter or security audit finds it for you.
Join the HeaderGuard waitlist โ Check your headers free| Value | What it means | Recommendation |
|---|---|---|
max-age=31536000 | Cache for 1 year | โ Good |
max-age=15768000 | Cache for 6 months | โ Minimum for SSL Labs A+ |
max-age=3600 | Cache for 1 hour | โ ๏ธ Too short for meaningful protection |
max-age=0 | Revoke HSTS | โ ๏ธ Only use intentionally during migration |
includeSubDomains | Apply to all subdomains | โ Include unless subdomains need HTTP |
preload | Eligible for browser preload list | โ Add and submit to hstspreload.org |
Check your current HSTS configuration with the free security headers checker โ it shows exactly which directive values are set and flags anything missing or misconfigured.