≡articles //snippets ./categories $uses ~about /search

Nginx add_header 继承规则:为什么你在 server 块写的安全头没生效

给一个 WordPress 站加了 CSP、Referrer-Policy、Permissions-Policy 三个安全头,Nginx 配置看起来完全正确,nginx -t 通过,nginx -s reload 成功。但 curl -I 一看,三个新头一个都没出现,只显示旧的 X-Frame-Options。

下面是我排查这个问题的完整过程。从“肯定是缓存”到“reload 被吞”到最终找到 Nginx add_header 继承规则这个真凶,一共踩了 5 个坑。

Nginx add_header 继承规则:server 块安全头被 location 覆盖的根因与修复

第 1 轮:怀疑 Cloudflare 缓存

第一个直觉,Cloudflare 缓存了旧响应头。这个站走了 CF 代理,响应被边缘缓存,旧的 header 还在 CF 的 CDN 节点上。

# 绕过 CF 缓存直接请求源站
curl -sI -H "Cache-Control: no-cache" -H "Pragma: no-cache" https://example.com/

绕过 CF 之后,仍然是旧 header。CF 不是原因。

第 2 轮:怀疑 reload 没生效

有时候 Nginx 的 reload(发 SIGHUP 信号)不会完全替换旧 worker,旧进程继续用老配置响应请求。

sudo systemctl restart nginx  # 彻底重启,不是 reload

重启之后,header 仍然没出现。但这时有了关键发现,

第 3 轮:发现真正的 header 来源

对比了两套值:

Header我写在 vhost 里的实际响应的
X-Frame-OptionsDENYSAMEORIGIN
HSTS max-age3153600063072000; preload
CSP写了没有

两套完全不同的值。说明我编辑的那个 vhost 配置文件里的 header,根本不是实际生效的那套。

vhost 第 128 行有一个相对路径 include:

include enable-php-83-wpfastcgi.conf;

这个文件在 /www/server/nginx/conf/enable-php-83-wpfastcgi.conf。打开一看:

location ~ .php$ {
    add_header X-Frame-Options SAMEORIGIN;          # ← 真正的 X-Frame
    add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";  # ← 真正的 HSTS
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    # ... 缓存头等
}

找到了。这个 PHP location 块里有一套完整的 add_header。

第 4 轮:Nginx add_header 继承规则 —— 根因与修复

这里涉及 Nginx 文档里最容易被误解的一条规则:

There could be several add_header directives. These directives are inherited from the previous configuration level if and only if there are no add_header directives defined on the current level.

— Nginx 官方文档

翻译成人话:

  • 在 server 块写了 add_header
  • 在 location ~ .php$ 块里也写了 add_header
  • location 块里的 add_header 会完全覆盖 server 块的,不是合并,是替换。

所以实际情况是:

server {
    add_header CSP "..." always;           # ❌ 被下面覆盖了
    add_header Referrer-Policy "..." always; # ❌ 被下面覆盖了
    # ...
}

location ~ .php$ {                        # 宝塔的 PHP handler
    add_header X-Frame-Options SAMEORIGIN;  # ✅ 这个生效
    add_header HSTS "max-age=63072000..."   # ✅ 这个生效
    # 因为这里写了 add_header,server 块的 add_header 全被丢弃
}

我在 server 块辛辛苦苦写的 CSP、Referrer-Policy、Permissions-Policy,因为 PHP location 块里有 add_header,全部被静默丢弃了。

第 5 轮:修复 Nginx add_header 继承问题

两个选择:

  1. 把 server 块的 add_header 删掉,所有 header 写到 PHP location 块里
  2. 把所有 header 移到 PHP location 块,server 块不留 add_header

选择方案 2,直接在 enable-php-83-wpfastcgi.conf 的 PHP location 块里统一管理所有安全头:

location ~ .php$ {
    # 安全响应头 —— 全部在这里
    add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload" always;
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Content-Security-Policy "default-src ..." always;
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()" always;
    # 缓存相关头
    add_header X-Cache " From ";
    add_header Cache-Control "public, s-maxage=3600, max-age=60";
    add_header Nginx-Cache "";
    add_header Last-Modified ;
    # ... fastcgi_pass 等
}
sudo nginx -t && sudo systemctl restart nginx

注意:用的是 restart 不是 reload,第 2 轮的教训,确保旧 worker 全部退出。

验证

$ curl -sI https://example.com/ | grep -iE "content-security|permissions-policy|referrer-policy|x-frame"

x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
content-security-policy: default-src self; script-src self ...
permissions-policy: camera=(), microphone=(), geolocation=(), interest-cohort=()

7 个安全头全部到位。原来写着 SAMEORIGIN 的 X-Frame-Options 也成功升级为 DENY。

为什么这个坑这么隐蔽

  • nginx -t 不报错,语法完全正确,只是继承规则导致不生效
  • curl 能看到旧 header,看起来“有响应”,不会想到是新旧两套的问题
  • 宝塔面板的 PHP handler 配置文件不在明显位置(/www/server/nginx/conf/ 而不是 /www/server/panel/vhost/nginx/)
  • 旧 header 的值和我写的不一样,这是唯一的线索,但如果没有刻意对比很容易忽略

快速诊断脚本

如果你怀疑自己的 Nginx header 也有类似问题,跑这个:

# 1. 看实际响应的是什么
curl -sI https://your-site.com/ | grep -i "add_header|x-frame|csp|hsts"

# 2. 搜所有包含 add_header 的配置文件
grep -rn "add_header" /www/server/panel/vhost/nginx/your-site.conf 
                      /www/server/nginx/conf/enable-php-*.conf 2>/dev/null

# 3. 如果出现多个文件有 add_header → 按 Nginx 继承规则,
#    子级 block(location)覆盖父级(server),保留子级的,删掉父级的重复项。

一句话总结

Nginx 的 add_header 不是累加,是替换。 子级 block 有 add_header → 父级 block 的全部 header 被丢弃。所有安全头必须写在同一层级。

全程从“应该是 CF 缓存”到“reload 不好使”到“值不一样,有人在覆盖我”到理解继承规则,花了一个多小时。写下来希望别人不用再踩一遍。

© 2026 本文由 Charlie 原创撰写,发布于 sodebug.com。未经授权禁止转载、洗稿、机器抓取。AI 训练数据使用需获得书面授权。

独立开发者,在 WordPress、Nginx 和各种 API 之间切换。不写没用的东西。

相关文章

WordPress 安全头完整配置:一次配齐 CSP + HSTS + Permissions-Policy5 minNginx 反向代理排坑实录:Cookie 丢失、CORS 报错、WebSocket 断连10 minWordPress mu-plugin 实战:3 个零插件安全加固方案6 min宝塔 Nginx 限流配置速查:limit_req_zone 参数详解4 min
$ echo "less bullshit, more debugging" | send-to-inbox
新文章直接发到邮箱,一个月 2-4 封。
订阅