原创

Nginx add_header 继承实验:为什么加一条头会丢掉父层配置

给静态资源加一条 Cache-Control,原本在 server 层配置的 X-Content-Type-Options 却消失了。配置文件里两条都还在,nginx -t 也通过。这时先看配置层级:在默认继承模型下,子层出现自己的 add_header,就不再继承上一层那组 add_header。

always 修正的是响应状态码范围,并不会把继承方式改成“自动合并”。两件事混在一起,往往造成首页正常、资源路径缺头,或者成功响应正常、404 缺头。

一份配置,四个响应

以下结果来自 Nginx 1.27.4 的独立本地实例。先创建空目录,端口 18083 应未被占用;所有配置与日志都在这个目录中,不加载机器上已有的站点配置。

mkdir header-lab
cd header-lab
mkdir logs

保存公共头为 headers.inc:

add_header X-Lab-Base base always;
add_header X-Content-Type-Options nosniff always;

再保存完整的 nginx.conf:

pid logs/nginx.pid;
error_log logs/error.log;
events {}
http {
    access_log logs/access.log;
    server {
        listen 127.0.0.1:18083;
        include headers.inc;

        location = /inherited {
            return 200 "ok";
        }
        location = /lost {
            add_header X-Lab-Leaf leaf always;
            return 200 "ok";
        }
        location = /fixed {
            include headers.inc;
            add_header X-Lab-Leaf leaf always;
            return 200 "ok";
        }
        location = /error {
            add_header X-Lab-Leaf leaf;
            return 404 "missing";
        }
    }
}

检查并启动:

nginx -p "$PWD/" -c nginx.conf -t
nginx -p "$PWD/" -c nginx.conf
curl -i http://127.0.0.1:18083/inherited
curl -i http://127.0.0.1:18083/lost
curl -i http://127.0.0.1:18083/fixed
curl -i http://127.0.0.1:18083/error

四条请求分别说明一件事:

  • /inherited 返回 200,包含 X-Lab-Base 和 X-Content-Type-Options。当前 location 没有声明 add_header,沿用父层那组配置。
  • /lost 返回 200,只有 X-Lab-Leaf。它声明了自己的头,父层那一组不再继承,并不是仅覆盖同名字段。
  • /fixed 返回 200,同时包含三种头。公共文件被明确包含到当前层,之后再增加路径自己的字段。
  • /error 返回 404,没有上述实验头。子层先截断父层继承,自己的 X-Lab-Leaf 又没有 always,不适用于这个响应状态。

最后一条尤其容易误判。看到 server 里的公共头都有 always,不能推断所有后代响应都能拿到它们。配置必须先进入当前层,才轮得到状态码条件决定是否发送。

公共 include 复用的是声明,不是一个运行时补丁

把公共头放在单独文件,主要是避免维护者在多个 location 中手工复制不同版本。凡是当前层需要增加 add_header,就显式包含公共文件,再写局部声明。配置审阅时也容易看见:这一层需要的是完整集合,而不是一个自动合并的差量。

不要因为要补回一个字段,就把父层所有头随意复制进来。缓存、内容安全策略、跨域与下载响应有不同要求,公共集合应该尽量小,并且确实适用于每个引用它的位置。实验中的自定义字段只是观察标记,nosniff 也不代表一套完整的站点安全策略。

同一响应字段还可能由应用上游发送。add_header 是增加响应字段,不是通用的“覆盖上游同名字段”操作。若代理返回的头与本层头冲突,需要先确定哪一层拥有该字段,再按代理模块的隐藏或传递规则处理。浏览器对重复字段的处理又各不相同,不能因为终端里能看到两行,就认定策略已经正确。

内部跳转同样要纳入观察。请求从一个 location 进入,经过错误页或 URI 回退以后,最终可能在另一个配置上下文结束。只检查入口 location,会漏掉实际生成响应的那一层。给每条受影响路径发真实请求,比只搜索配置文件中的字段名更可靠。

1.29.3 以后有合并指令,旧版本不能直接照抄

add_header_inherit 从 Nginx 1.29.3 开始提供。支持该指令的版本可以选择 merge,让当前层追加父层的值;默认 on 仍遵守原来的继承模型。本文的 1.27.4 实验没有运行这个新指令。

# 仅用于支持该指令的 Nginx 1.29.3 及以后版本。
add_header_inherit merge;

升级后采用合并也应先检查已有 include。若配置原来已经在子层重复包含公共头,再启用自动合并,可能把同一字段添加多次。迁移时应选择一种清楚的组织方法,并重新请求成功、重定向和错误路径,而不是把一个新指令无条件追加到旧配置顶部。

遇到 unknown directive,先核对实际运行的二进制版本与配置入口。服务器上可能装了多个 Nginx,命令行检查的是一份,服务管理器启动的是另一份。版本差异要靠实际进程与启动参数确认。

排查完停止本实验实例:

nginx -p "$PWD/" -c nginx.conf -s quit

再看那次“加缓存头以后安全头消失”的改动,重点就很明确了:它不只是新增一行,而是让子层从继承父集合,切换到了拥有自己的集合。修正继承,和决定哪些状态码发送字段,是两次各自需要核验的判断。

正文到此结束
评论插件初始化中...
Loading...