Nginx 又双叒给我挖坑了:HSTS 撞车 + 宝塔偷偷锁 Frame

发布时间:2026-06-14 12:00   浏览量:4
蛋蛋之家
https://wuqishi.com/rss.xml
一枚蛋蛋的自留地


环境:Nginx 1.28.3 + 宝塔面板 + Halo 2.x + Cloudflare

把博客从 Typecho 搬到 Halo 2.x,Docker Compose 跑起来,反代配好,心想总算能歇了。
结果一接 Cloudflare,控制台开始疯狂报红。再一试跨站嵌入,直接给我拒了。两个坑连环爆,折腾了一晚上,记录一下,下次别再踩了。


动漫版头图,可爱吧


第一坑:HSTS 重复输出,Cloudflare 和我各说各话
咋回事呢?
我习惯在 Nginx 里开 HSTS,强制走 HTTPS:
add_header Strict-Transport-Security "max-age=31536000" always;
接入 Cloudflare 后,浏览器开始骂街——​Duplicate Header​。
因为 Cloudflare 边缘节点本身就会统一添加 HSTS 头,回源时我再加一次,就撞车了。一个响应头出现两次,浏览器很懵:你俩到底听谁的?
我第一反应:上 if 啊!
这还不简单?判断一下有没有 Cloudflare 的特征头,没有再加 HSTS 呗:
# 直觉写法,当场翻车
if ($http_cf_ray = "") {
add_header Strict-Transport-Security "max-age=31536000" always;
}
​nginx -t​ 直接甩脸:
[emerg] "add_header" directive is not allowed here
???不让我加?
后来才知道,Nginx 的 if​ 出了名的邪门,江湖人称 ​"If is Evil"​。你想在 if​ 里塞 add_header​?门儿都没有。这玩意儿只能在 server​ 和 location​ 里裸奔,不能塞进 if​ 的怀抱。
解法:map 大法好
既然 if​ 靠不住,那就把逻辑抽到 http​ 块,用 map​ 做判断(注意:map​ 不能放在 server​ 块内部):
http {
# 动态 HSTS 智能过滤映射
# Nginx 1.7.5+ 支持:值为空字符串时 add_header 自动隐匿
map $http_cf_ray $dynamic_hsts {
default ""; # 有 cf-ray?走 Cloudflare,HSTS 留空
"" "max-age=31536000"; # 没 cf-ray?直连源站,上 HSTS(有效期1年)
}

server {
# ...
add_header Strict-Transport-Security $dynamic_hsts always;
}
}
妙处来了: Nginx 1.7.5+ 中,add_header​ 有个隐藏特性——值是空字符串 ""​ 时,这个头直接​隐身​,不会出现在响应里。
所以走 Cloudflare 时 $dynamic_hsts​ 是 ""​,HSTS 自动消失;直连源站时输出 max-age=31536000​,完美。if​ 你不行,我 map​ 上,照样搞定。

第二坑:宝塔偷偷给我加了 X-Frame-Options
又是咋回事呢?
我想搞跨站嵌入,结果浏览器死活不让:
拒绝在 Frame 中加载,因为响应头包含了 X-Frame-Options: SAMEORIGIN
我明明在反代里写了:
proxy_hide_header X-Frame-Options;
Halo 后端吐的头应该被抹掉了啊,这 SAMEORIGIN​ 从哪冒出来的?我一度怀疑人生,是不是 Halo 在跟我作对。
排查:宝塔是内鬼
顺着配置翻啊翻,发现宝塔面板自动生成了一堆扩展 conf(注意:wuqishi.com​ 替换为你的实际域名):
include /www/server/panel/vhost/nginx/extension/wuqishi.com/*.conf;
include /www/server/panel/vhost/nginx/generic/wuqishi.com/*.conf;
里面静静躺着:
add_header X-Frame-Options SAMEORIGIN;
好家伙,宝塔你背刺我!
关键认知: proxy_hide_header​ 只能藏后端服务器吐的头,对 Nginx 自己配置里写的头完全没辙。宝塔全局一加,我的反代配置根本管不到,就像你在家锁了门,结果物业在小区大门又加了一道锁。
解法:不跟宝塔纠缠,直接上 CSP
去翻宝塔文件删了?太怂,而且面板一更新可能又刷回来,跟打地鼠似的。直接在反代入口写三重保险:
# 第一重:后端来的 X-Frame-Options,抹掉
proxy_hide_header X-Frame-Options;

# 第二重:宝塔全局加的?强行重置为空,浏览器看到空值直接忽略
add_header X-Frame-Options "" always;

# 第三重:终极杀招,CSP 的 frame-ancestors 优先级碾压 X-Frame-Options
# * 表示允许所有域名嵌入,如需限制改为 https://example.com
add_header Content-Security-Policy "frame-ancestors *" always;
原理: 现代浏览器里,CSP 的 frame-ancestors​ 指令优先级远远高于老古董 X-Frame-Options​。看到 frame-ancestors *​,浏览器当场无视 SAMEORIGIN​,跨站嵌入一路绿灯。
​*​ 意思是"谁都能嵌",如果你只想让特定域名嵌,如改成 frame-ancestors https://wuqishi.com​ 就行。但我懒,先开全通,后面再收紧。

收工,总结一下
两个坑,都是响应头的小把戏,但排查起来很费神。最后靠 map​ 和 CSP 组合拳搞定,没动业务逻辑,纯粹是边缘层的斗智斗勇。













核心解法


一句话总结




HSTS 撞车


​map​动态变量


有 Cloudflare 就隐身,直连再上 HSTS




宝塔锁 Frame


CSPframe-ancestors​


优先级碾压,不跟宝塔文件纠缠





​nginx -t​ 绿灯,配置保存不报错。响应头清清爽爽,强迫症舒服了。

解决问题的乐趣就在于此,把流量在边缘层梳理得明明白白,爽。
查看原文

该站点最新相关文章