How to Configure max-age and includeSubDomains for HSTS
This guide sets the two directives that control HSTS scope and lifetime. The protocol mechanics, RFC 6797 threat model, and preload trade-offs behind these values live in the HTTP Strict Transport Security (HSTS) deep dive; here the focus is the exact wire values and the safe path to a one-year policy.
Configuration Syntax & Exact Values
Strict-Transport-Security: max-age=63072000; includeSubDomains
max-age=63072000— enforcement lifetime in seconds, counted from each response.63072000is two years, the value Chromium’s preload guidance and most hardening baselines recommend;31536000(one year) is the minimum that qualifies for preload. The browser refreshes this countdown on every response, so an active site effectively never lapses.includeSubDomains— a valueless flag that extends the policy to every host below the issuing one (api.example.com,cdn.example.com, and hosts the browser has never visited). It is the directive that closes downgrade gaps on forgotten subdomains, and the one that causes lockouts when a subdomain lacks matching TLS.
Both directives are case-insensitive and order-independent. Do not quote the value inside the directive (max-age="63072000" is malformed and silently dropped). Add preload only when you have committed to the near-permanent enforcement trade-offs and reached the full two-year value.
Server-Side Configuration
Emit the header once, at the outermost layer that touches every response. Declaring it at both the CDN and the origin produces two copies; pick one source.
Nginx
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
always forces emission on 4xx/5xx responses; without it the header vanishes from error pages. An add_header in a location block replaces inherited headers, so repeat this line in any location that sets its own.
Apache
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
always appends the header even on internally generated error documents, which the default onsuccess table skips. Requires mod_headers (a2enmod headers).
Cloudflare
Dashboard → SSL/TLS → Edge Certificates → HTTP Strict Transport Security (HSTS). Enable it, set Max-Age to 12 months or longer, and toggle Include subdomains to On. Cloudflare emits the header at the edge on all proxied responses; delete any duplicate origin add_header so only one copy reaches the browser.
Node/Helmet
const helmet = require('helmet');
app.use(helmet.hsts({ maxAge: 63072000, includeSubDomains: true }));
Register Helmet before route and error handlers so short-circuited responses still carry the header. Behind a TLS-terminating proxy, set app.set('trust proxy', 1) so Express treats forwarded requests as secure.
Diagnostic & Verification Steps
Confirm the exact wire value:
curl -sI https://yourdomain.com | grep -i strict-transport-security
Expected output:
strict-transport-security: max-age=63072000; includeSubDomains
Verify subdomain coverage before trusting includeSubDomains — the flag enforces on hosts even if their certificate does not cover them:
openssl s_client -connect api.yourdomain.com:443 -servername api.yourdomain.com < /dev/null 2>/dev/null \
| grep -E 'Verify return code|subject='
Expected output: Verify return code: 0 (ok) on every subdomain you intend to cover.
Browser DevTools: Network tab → filter Doc → select the document request → Response Headers → confirm the exact value and that no duplicate Strict-Transport-Security line is present. Cross-check the cached entry at chrome://net-internals/#hsts via Query HSTS/PKP domain.
Edge Cases, Security Implications & Safe Rollback
A misconfigured subdomain surfaces as NET::ERR_CERT_COMMON_NAME_INVALID with no proceed button — HSTS errors are non-bypassable by design. Three traps dominate:
- Subdomain certificate gaps.
includeSubDomainsenforces HTTPS oninternal.,monitoring., and legacy API hosts whether or not their certificate matches. Audit every host (subfinder+httpx, or your DNS zone export) before enabling the flag. - Cache stickiness on rollback. Reducing
max-agedoes not clear existing browser caches. To revert you must actively serveStrict-Transport-Security: max-age=0; includeSubDomainsover HTTPS, and it only reaches clients that revisit before their existingmax-ageexpires. A botched two-year policy can lock a user out for two years with no server-side fix. - Preload lock-in. Submitting to the preload list is effectively permanent; removal takes months of browser release cycles. Never preload until
includeSubDomainsis proven across staging, legacy, and development hosts.
The safe path is to ramp max-age in stages, validating subdomain reachability at each step before the value grows long enough to be costly to undo.
Frequently Asked Questions
What max-age value should I use in production?
63072000 (two years) is the recommended hardening value and exceeds the one-year (31536000) preload floor. Anything shorter than one year disqualifies the domain from preloading.
Will increasing max-age reset the timer for returning visitors? Yes. Browsers recompute expiry from each response, so a visitor who hits a longer value adopts the new lifetime immediately. There is no need to wait for the old value to lapse before raising it.
Can I roll back just by lowering max-age?
No. Lowering the value affects only future responses; cached policies persist until their original max-age expires. Active rollback requires serving max-age=0 over HTTPS and waiting for clients to revisit.
Is includeSubDomains safe to add immediately? Only after auditing TLS on every subdomain. The flag enforces HTTPS on hosts the browser has never seen, and any host without a matching certificate produces a non-bypassable error.
Conclusion
Start at max-age=300 in staging to prove every subdomain — internal tooling, dashboards, APIs — answers over HTTPS, then ramp to 86400, 2592000, and finally 63072000 with includeSubDomains in production. Add preload and submit to hstspreload.org only after the two-year policy has been stable in production, because that step is effectively permanent.