配好了 HSTS,响应头却迟迟不出现?很多人在 Nginx 上配置完 Strict-Transport-Security 之后,第一反应就是打开浏览器访问网站,结果一看响应头——什么都没有。本质上这不是什么疑难杂症,大多数时候问题出在几个容易忽略的地方。这篇文章把 HSTS 不生效的常见原因逐一梳理,并给出对应的解决方法。
HSTS 是什么,配置逻辑要先搞清楚
在说排查方法之前,先把 HSTS 的基本逻辑理清楚。Strict-Transport-Security 响应头告诉浏览器:从现在开始,这个域名以及它的子域名,在指定的时间内(max-age)只能通过 HTTPS 访问。如果浏览器发现有人想通过 HTTP 连接,会自动把请求升级为 HTTPS,根本不需要用户操作。
配置本身不复杂,但在 Nginx 中有几种不同的写法,不同写法对应的行为差异很大。如果这块没搞清楚,后面的排查就是盲人摸象。
排查步骤一:响应头有没有发出
这是最基础的一步。很多情况下 HSTS 根本没有出现在响应头里,只是浏览器缓存或者其他原因让你以为它不生效。
用 curl 直接查看是最快的验证方法:
curl -I https://your-domain.com/ 2>&1 | grep -i strict
如果命令没有输出,说明服务器根本没有发送这个响应头。这时候需要检查 Nginx 配置本身,而不是浏览器。
排查步骤二:配置是否写在正确的 location 块
这是最容易踩的坑。HSTS 头必须加在 HTTPS 的响应里,如果你的配置写在了 HTTP 的 server 块里,HTTPS 的请求根本不会经过那个块,响应头自然不会发出来。
正确做法是在监听 443 端口的 server 块里配置:
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# 正确的写法:直接在 server 块级别
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}
# HTTP 重定向单独处理
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
还有一点要注意:如果在同一个 server 块里既有 proxy_pass 又有 add_header,Nginx 的 add_header 指令默认不会在 proxy_pass 产生的响应上生效。这时候需要额外处理,或者改用 headers-more 模块。
排查步骤三:add_header 指令的继承规则
Nginx 的 add_header 有继承规则——当一个 location 块里的响应是通过代理(proxy_pass、fastcgi_pass 等)产生的时候,父级 server 块的 add_header 不会被继承。
举个例子:
server {
listen 443 ssl;
add_header Strict-Transport-Security "max-age=31536000" always;
location / {
proxy_pass http://backend;
# 这里不会有 HSTS 头,因为响应来自 proxy_pass
}
location /static {
# 静态文件直接返回,HSTS 头会生效
}
}
解决方案有两个:一是把 add_header 写在 proxy_pass 对应的 location 块里;二是使用 headers-more 模块来强制覆盖响应头。
使用 headers-more 模块的写法:
location / {
proxy_pass http://backend;
more_set_headers "Strict-Transport-Security: max-age=31536000; includeSubDomains";
}
排查步骤四:HTTPS 证书是否有效
有些浏览器(尤其是 Chrome)对 HTTPS 证书有问题的情况会拒绝处理 HSTS 头。如果你的证书过期、域名不匹配或者自签名证书没有被客户端信任,HSTS 头可能被忽略。
检查证书状态:
openssl s_client -connect your-domain.com:443 -servername your-domain.com 2>&1 | openssl x509 -noout -dates
确保证书在有效期内,且域名与配置的 server_name 完全一致。
排查步骤五:HTTP 严格传输安全 preload 指令的正确使用
如果你的配置里有 preload 指令,需要特别注意:preload 不是你想加就能加的,它有额外的要求。
# 包含 preload 指令
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
加上 preload 意味着你申请将域名提交到 Chrome 的 HSTS preload list。一旦提交,移除是极其困难的(需要数月才能生效)。在此之前,请确保所有子域名都支持 HTTPS,否则子域名也会被强制 HTTPS,导致服务不可用。
所以在生产环境,建议先用不带 preload 的配置验证稳定后,再考虑提交。
排查步骤六:浏览器缓存与 HSTS 状态
有时候 Nginx 配置完全正确,curl 也能看到响应头,但浏览器就是不生效。这大概率是浏览器端的问题。
可能的原因:
- 浏览器之前缓存了域名的 HSTS 状态,且 max-age 已经过期,状态被清除
- 首次访问时网站用的是 HTTP,HSTS 头根本没有被浏览器接收
- 使用了隐身/无痕模式,HSTS 状态没有持久化
清除浏览器 HSTS 缓存的方法:
Chrome 地址栏输入:chrome://net-internals/#hsts,找到 "Delete domain security policies",输入域名后删除。
Firefox 则需要删除 profile 目录下的 SiteSecurityServiceState.txt 文件后重启浏览器。
常见错误配置汇总
以下几种写法是实践中常见的问题:
错误一:配置写在 HTTP server 块里。HTTP 连接本身不会被 HTTPS 加密,加了也白加。
错误二:max-age 设置过小。如果 max-age 小于 0,浏览器会认为域名不再使用 HSTS,立即失效。生产环境建议至少设置为 31536000(一整年)。
错误三:在 proxy_pass 的 location 里用了普通 add_header。不继承,HSTS 头直接丢失。改用 more_set_headers 或在 location 块里单独加。
错误四:includeSubDomains 导致子域名集体躺枪。加了 includeSubDomains 后,所有子域名都被强制 HTTPS。如果某个子域名还没配置 HTTPS,用户访问就会被拒绝。稳妥的做法是先不加 includeSubDomains,等所有子域名都 HTTPS 化了再加。
完整的 HSTS 配置示例
给出一套分阶段的稳妥配置方案:
阶段一:初始配置(第一周)
add_header Strict-Transport-Security "max-age=300" always;
先用 5 分钟 max-age 测试,观察有没有问题。
阶段二:稳定运行(第二周起)
add_header Strict-Transport-Security "max-age=31536000" always;
一年 max-age,确保所有资源都走 HTTPS。
阶段三:包含子域名(确认所有子域名 HTTPS 后)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
阶段四:申请 preload(确认不打算撤回后)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
每个阶段至少观察一周再进入下一阶段,有什么问题都能及时发现。
总结
HSTS 配置不生效的问题,排查思路其实很清晰:先确认响应头是否发出,再看配置是否写在正确的位置和正确的 server 块,然后处理代理场景下的继承问题,接着验证 HTTPS 证书有效性,最后考虑浏览器端因素。按照上述六步排查,基本上能定位到根本原因。
最重要的原则是:分阶段上线,先小范围验证再扩大范围。HSTS 一旦大规模生效,再想撤回(尤其是加了 preload)代价极大。
相关文章推荐
- Strict-Transport-Security不生效原因排查:6个常见问题逐一击破
- HSTS配置后浏览器仍然走HTTP?从这5个方向排查彻底解决问题
- Nginx HSTS max-age设置建议:分阶段配置方案与最佳实践
原创内容,转载请注明出处。如有 Nginx 配置问题,欢迎在评论区交流。
版权声明
本文仅代表个人观点。
本文系AI辅助作者原创,未经许可,转载请保留原文链接。

发表评论