You added Strict-Transport-Security to the config, deployed, and the header is not in the response. Or it is there on one URL and gone on another.
Response headers are not a setting, they are the end of a pipeline. Your request passes through a CDN, maybe a load balancer, maybe a reverse proxy, then your application, and every one of those can add a header, replace one, or drop one. Only what survives the last hop is what the browser sees.
See what is actually returned
curl -sI https://example.com | grep -i strict-transport
If that comes back empty, the header is not reaching the edge. Check a few more paths before concluding anything, because partial coverage is extremely common:
for p in / /login /api/health /missing-page; do
printf "%-16s %s\n" "$p" "$(curl -sI "https://example.com$p" | grep -i strict-transport || echo MISSING)"
done
Where it goes missing
| Layer | What it does |
|---|---|
| CDN | Has its own header rules and can strip or replace origin headers entirely. Often the answer when the origin looks correct |
| Reverse proxy | An add_header or equivalent in an inner block can silently discard headers set in an outer one |
| Application framework | Security middleware may set its own policy, overwriting yours with a shorter max-age |
| Route coverage | Configured on the main site block but not on redirects, error pages, or the API |
| Plain HTTP responses | Set on the port 80 response, where browsers ignore it completely |
That fourth row deserves attention. A redirect from example.com to www.example.com is itself a response, and if it carries no HSTS policy then the apex domain never gets protected no matter what www sends.
Nginx has a specific trap
In nginx, add_header directives do not accumulate across blocks. If a location block declares any add_header, every add_header from the enclosing server block is discarded for that location.
So a policy set at server level disappears the moment an inner location adds a single header of its own. The always parameter is also needed if you want the header on error responses, not just successful ones.
Then check the policy you got back
Once the header appears, read the value rather than assuming. A framework default might be giving you a max-age of a few hundred seconds, which is technically present and practically meaningless.
Confirm the duration is what you configured, and that includeSubDomains is there only if you genuinely audited your subdomains. HSTS and HTTPS browser policy covers the directives and the rollout order, and browser security headers covers the neighboring headers that fail the same way.