本文记录一次生产站点排障经历:同一个响应里出现了两条
Strict-Transport-Security头,顺着这条线索又牵出了 HTTP 不跳转、www 无法访问、控制面板配置陷阱等一系列问题。全程使用curl抓包定位,不依赖图形界面,思路同样适用于其他反代 + 后端的架构。
一、现象
用浏览器访问站点时,开发者工具里出现了两条 HSTS 头:
Strict-Transport-Security: max-age=31536000
Strict-Transport-Security: max-age=31536000; includeSubDomains
其中一条不带 includeSubDomains,另一条带。浏览器虽然会合并这类头,但重复头既不规范,也容易被安全扫描工具标记为异常。
二、复现与定位
先用 curl 抓完整响应头确认是否稳定复现:
curl -s -D - -o /dev/null https://yourdomain.com/
输出(节选):
HTTP/1.1 200 OK
Server: openresty
...
X-Content-Type-Options: nosniff
Strict-Transport-Security: max-age=31536000
set-cookie: XSRF-TOKEN=...; Path=/; HTTPOnly
Strict-Transport-Security: max-age=31536000; includeSubDomains
关键观察点:
- 两条 HSTS 头在响应中的位置不同。
- 第一条不带
includeSubDomains,和X-Content-Type-Options、X-Frame-Options等安全头紧挨在一起,成批出现。 - 第二条带
includeSubDomains,出现在set-cookie之后,位于响应最末尾。
这说明两条头来自两个不同的层。
三、根因分析:两个层级各加了一份
客户端
│
▼
OpenResty / Nginx(反代) ← add_header Strict-Transport-Security "...includeSubDomains"
│ (追加到响应末尾)
▼
后端应用(Java / Spring Security) ← 默认安全头 Strict-Transport-Security "max-age=31536000"
(和 X-Frame-Options 等一起输出)
- 后端应用(本站是 Halo,基于 Java 技术栈)默认输出了一批安全头,其中包含
Strict-Transport-Security: max-age=31536000(不带includeSubDomains)。这就是第一批安全头里那一条。 - 反向代理 Nginx 配置里写有
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";,Nginx 会把这条头追加到代理响应的末尾。 - 由于 Nginx 默认透传上游所有响应头,两条 HSTS 都被原样发给了客户端,于是出现重复。
顺带发现一个有趣的细节:用 curl -I(HEAD 请求)时只看到一条 HSTS。原因是 HEAD 请求触发了后端的 404 错误路径(application/problem+json),处理链路和正常 200 不同,并非稳定行为,不能据此判断“没重复”。
四、解决方案
原则:HSTS 只保留一份,推荐统一放在反向代理这一层。
在反代的 location 块里屏蔽上游的那份,只保留 Nginx 自己加的一份:
location / {
proxy_pass http://127.0.0.1:8090;
proxy_hide_header Strict-Transport-Security; # 关键:去掉上游那一份
}
server 层的 add_header 保持不变:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
改完重载并验证:
nginx -t && nginx -s reload
curl -s -D - -o /dev/null https://yourdomain.com/
只看到一条 Strict-Transport-Security: max-age=31536000; includeSubDomains,问题解决。
五、顺藤摸瓜发现的其他问题
排查过程中又陆续发现三个问题,一并记录。
1. HTTP 请求 403,不跳转 HTTPS
curl -I http://yourdomain.com/
HTTP/1.1 403 Forbidden
站点应该强制 HTTPS,但纯 HTTP 访问被直接拒绝。排查发现:
- 原配置里有一句
if ($scheme = http) { return 301 https://$host$request_uri; },但它写在只监听 443 的 server 块里。443 上收到的永远是 https 请求,$scheme恒为 https,这段代码永远不会执行——是死代码。 - 80 端口实际由控制面板(1Panel)的默认 server 接管,直接返回 403。
解决办法:让 server 块同时监听 80 和 443,if ($scheme = http) 才会在 80 端口的请求上触发:
listen 80 ;
listen [::]:80 ;
listen 443 ssl ;
listen [::]:443 ssl ;
server_name yourdomain.com www.yourdomain.com;
2. www 域名无法访问
访问 www.yourdomain.com 直接超时。排查发现该子域名根本没有 DNS 解析记录:
yourdomain.com → 服务器IP
www.yourdomain.com → 无 A 记录
而 HSTS 又声明了 includeSubDomains,浏览器会把 www 也强制到 HTTPS,结果 www 根本没解析,体验更差。解决方法是去 DNS 控制台给 www 补一条 A 记录,并确认证书包含 www(SAN 或通配符)。
3. 踩到 1Panel 的一个坑:强制 HTTPS 跳转没生效
在 1Panel 里勾了“强制 HTTPS 跳转”,但 http 访问依旧不跳。查看生成的配置后发现,1Panel 的“强制跳转”只是在 server 块里加了一句 if ($scheme = http) return 301,而这句话只有在该 server 块同时监听 80 端口时才有效。
1Panel 的实际规则是:http 和 https、带 www 和不带 www,四种组合都要作为独立的站点配置加上,强制跳转才会对每一组都生效。只配 https 或只配不带 www 的条目,跳转就不完整。
六、全站安全响应头基线
顺手把各站点的响应头都过了一遍,整理成一张体检表:
| 站点 | 状态 | 主要问题 |
|---|---|---|
| 主站(Halo) | 大部分合规 | Cookie 缺少 Secure 和 SameSite;缺 CSP;Server 头泄露软件栈 |
| 网盘列表站(OpenList) | 问题最大 | 只有 HSTS,缺 X-Frame-Options、X-Content-Type-Options、Referrer-Policy、CSP |
| 密码管理器(Vaultwarden) | 最规范 | CSP、permissions-policy、frame-options 等默认齐全,无需改动 |
值得推荐的通用安全头基线:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'" always;
几个容易忽视的点:
- Cookie 安全属性:生产环境 Cookie 至少要
Secure; SameSite=Lax,否则明文链路下可能被窃取。 - 关闭软件栈泄露:
server_tokens off;隐藏 Nginx 版本/软件名。 - 管理类端口别裸奔公网:文件列表后端 API、密码管理后台这类端口,即使有登录鉴权,也应尽量用防火墙限制到管理 IP,避免攻击面直接暴露。
- HSTS preload 的前提:想申请浏览器 HSTS 预加载列表,要求
http://必须返回 301 到 https;关闭 80 端口会让 preload 无法通过。
七、小结
- 重复的响应头,多半是“上游应用 + 反代”两层各自添加导致,
proxy_hide_header是标准解法。 - 排查响应头问题,
curl -s -D -比浏览器开发者工具更直观,还能避免浏览器缓存干扰。 - 死代码是最隐蔽的坑:写在只监听 443 块里的
if ($scheme = http),永远不会执行。 - 用 1Panel 这类控制面板,要理解它生成配置的逻辑(http/https、www/非 www 分开配置),否则“看着配了,实际没生效”。
- 安全头检查是运维体检的常规项,建议定期扫一遍,重点盯 Cookie 属性、CSP、管理端口暴露。
一次 HSTS 重复响应头引发的排障记录:Strict-Transport-Security 出现两次的原因与解决
https://kaneniu.com/archives/1785690401838.html
评论