尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Linux Nginx 怎么在代理时丢弃不需要的客户端请求头

Linux Nginx 怎么在代理时丢弃不需要的客户端请求头 前言先纠正一个很常见的误解nginx 在反代时默认并不丢弃客户端请求头恰恰相反它会把客户端发来的请求头原样转发给上游。官方文档里proxy_set_header那段只说默认只重定义了 Host 和 Connection 两个字段言下之意是其余字段照原样透传。所以怎么丢弃这件事从来不是默认行为而是需要显式配置的动作。这个默认值带来两个真实后果。第一任何客户端都能自己塞一个X-Forwarded-For: 1.2.3.4或X-Real-IP进来如果上游应用信任这类头做审计、限流或 IP 白名单就形成了伪造漏洞。第二上游会收到一堆它根本不需要的头客户端代理留下的Proxy、扫描器探测用的X-Original-URL、以及和上游协议不一致的Connection轻则日志噪音重则行为异常。本文讲清楚三件事nginx 默认转发了什么、用什么指令把某个头摘掉、以及proxy_set_header那套全有或全无的继承规则是怎么把配置写崩的。示例基于 nginx 1.20RHEL 9 AppStream/ 1.22Debian 12及更新的 1.24、1.26这些指令在以上版本中语义一致。一、请求头与响应头是两条独立的通道新手最容易搞混的一点改请求头和改响应头用的是完全不同的指令方向不能错。指令作用方向典型用途proxy_set_header客户端请求 → 上游改写、追加、丢弃请求头proxy_pass_request_headers客户端请求 → 上游一刀切off时一个客户端头都不转发proxy_hide_header上游响应 → 客户端隐藏上游返回的响应头proxy_pass_header上游响应 → 客户端默认被藏起来的头Date、Server 等放行只记住一句要处理客户端发出去的东西用proxy_set_header。二、把请求头丢掉两个层次的手段2.1 单个字段把值设成空字符串这是最精确的做法也是官方文档明确写的语义——proxy_set_header的值是空字符串时该字段不会发送给被代理的服务器proxy_set_header X-Real-IP ; proxy_set_header X-Forwarded-For ; proxy_set_header Proxy ;注意是空字符串不是不写这一行。不写 不干预 客户端发什么上游收什么写 明确移除。这一字之差就是防伪造能不能生效的分水岭。同理把值重写成 nginx 自己算出来的真实值也能达到丢弃客户端版本的效果而且比直接删掉更有用proxy_set_header X-Forwarded-For $remote_addr; # 整体替换客户端塞的值被覆盖这里必须提醒一个广泛存在的错误写法proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 追加不是替换$proxy_add_x_forwarded_for的语义是把客户端 IP 追加到已有的 X-Forwarded-For 后面。如果客户端发来X-Forwarded-For: 9.9.9.9上游收到的是9.9.9.9, 真实IP。真实的那个在最后而绝大多数应用取的是第一个——于是伪造成功。要么改用$remote_addr要么确认上游取的是最后一个值。2.2 全量一个客户端头都不转发如果上游是纯粹的内部服务不需要任何客户端信息可以用总开关location /internal-api/ { proxy_pass http://backend; proxy_pass_request_headers off; # 默认是 on }注意off之后下面这些proxy_set_header依然生效——它们定义的是发给上游的头与客户端是否转发无关location /internal-api/ { proxy_pass http://backend; proxy_pass_request_headers off; proxy_set_header Host $host; # 仍然会发给上游 proxy_set_header X-Real-IP $remote_addr; # 仍然会发给上游 }也就是说proxy_pass_request_headers off加几条proxy_set_header是给上游一个干净、已知的请求头集合的稳定写法。三、继承规则一个 location 打翻整个 server这是本节最值得反复看的一条。proxy_set_header的继承是全有或全无这些指令仅在当前层级没有定义任何 proxy_set_header 时才继承上一层的配置。也就是说你在location里写了哪怕一条proxy_set_headerserver和http层写的所有proxy_set_header就全部失效取而代之的是 nginx 的内建默认值Host $proxy_host、Connection close。一个非常典型的翻车现场server { server_name api.example.com; proxy_set_header Host $host; # 打算让上游知道真实域名 location /v1/ { proxy_pass http://127.0.0.1:8080; proxy_set_header X-Request-Id $request_id; # 只加了一条 # 结果Host 变成 $proxy_host也就是 127.0.0.1:8080 } }上游拿到的 Host 变成了127.0.0.1:8080基于域名做路由或签名的应用立刻报错。修法只有两种要么在这个 location 里把需要的东西全部重写一遍要么把proxy_set_header提到一个所有 location 都能继承的层级上并且保证 location 里一条都不写。出于这个原因实践中推荐把所有proxy_set_header集中定义在一处http或server层location 里只写proxy_pass。需要差异化的地方用include引入一个完整的头集合片段而不是补一条。理解Host三种取值也是必要的变量含义$host请求行或 Host 头里的主机名已转小写、去掉端口取不到时回退到server_name$http_host客户端发来的 Host 头原样带端口$proxy_hostproxy_pass里写的主机名用 upstream 块时是 upstream 的名字实战一份可直接用的反代头配置以下配置以 Debian 12nginx 1.22站点文件在/etc/nginx/sites-enabled/为例RHEL 9 把server块放进/etc/nginx/conf.d/api.conf即可其余一样。# /etc/nginx/conf.d/api.conf upstream api_backend { server 127.0.0.1:8080; keepalive 32; # 配合下面 Connection 复用长连接 } server { listen 80; server_name api.example.com; # 关键所有 proxy_set_header 集中在 server 层 # 保证下面每个 location 都不再单独定义继承才生效 proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; # 替换不是追加 proxy_set_header X-Forwarded-For $remote_addr; # 客户端伪造的值被覆盖 proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Request-Id $request_id; # 丢掉客户端可能塞进来的、上游不该信的头 proxy_set_header X-Forwarded-Server ; proxy_set_header X-Original-URL ; proxy_set_header X-Rewrite-URL ; proxy_set_header Proxy ; location /api/ { proxy_pass http://api_backend; # 注意这里一条 proxy_set_header 都不能写 # 写了就会丢掉上面全部继承来的设置 } location /healthz { proxy_pass http://api_backend; } }如果某个路径确实需要长连接到上游keepalive生效的前提Connection头要单独处理location /api/ { proxy_pass http://api_backend; # 这里必须写 proxy_set_header代价是放弃继承所以把需要的都列全 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header Connection ; # 空值 删除长连接才能复用 }而 WebSocket 场景需要相反的值用map按客户端的Upgrade头动态决定# 放在 http 块里 map $http_upgrade $connection_upgrade { default upgrade; close; }然后在 location 里写proxy_set_header Connection $connection_upgrade;。注意Connection: upgrade和Connection: 是互斥的两个需求前者要升级协议后者要复用上游长连接同一个 location 里不能既要又要。验证让上游把收到的头打印出来不要靠猜直接看 nginx 究竟发了什么。用一个裸 socket 当上游它会原样打印收到的请求行和请求头# 终端 A监听 8080nc.openbsd 语法部分系统是 nc -l -p 8080 # 以本机 nc 的手册为准 nc -l 8080 # 终端 B发一个带自定义头的请求 curl -s -o /dev/null -H X-Secret: 1 -H X-Forwarded-For: 9.9.9.9 \ -H Host: api.example.com http://127.0.0.1/api/ping终端 A 里会看到上游实际收到的完整请求头。逐条核对X-Forwarded-For是不是只剩你的真实 IP、X-Secret是否还在、Host是不是api.example.com。改完配置后务必先校验再重载sudo nginx -t sudo systemctl reload nginx sudo nginx -T | less # 打印合并后的完整配置确认没有别的文件覆盖了你的设置nginx -T这一步很重要因为conf.d和sites-enabled里其它文件的server块可能匹配同一个server_name把配置抢先接管。常见坑点1. 把X-Forwarded-For用追加变量设置❌proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;——客户端伪造的值留在最前面。 ✅ 用$remote_addr整体替换确实需要保留链路时确认上游取的是最后一个地址。2. 在 location 里只补一条proxy_set_header❌ 在server层定义了Host $host在location里又写了proxy_set_header X-Request-Id $request_id结果 Host 变回$proxy_host。 ✅ 继承是全有或全无。要么该层级把需要的头列全要么在这一层一条都不写。3. 用proxy_set_header Host $http_host却没意识到差异❌$http_host会把客户端发来的端口一起带上客户端发Host: evil.com时上游就收到evil.com。 ✅ 明确要知道真实域名就用$host会回退到server_name需要原样透传时才用$http_host并确保server_name严格匹配。4. 把请求头和响应头的指令用反❌ 想隐藏上游返回的Server头却写了proxy_set_header Server ;客户端依旧看到上游的版本号。 ✅ 响应头要用proxy_hide_header Server;。默认被隐藏的响应头有Date、Server、X-Pad、X-Accel-...需要放行时用proxy_pass_header。5. 带下划线的头被静默丢弃反向的坑❌ 上游需要一个X_Api_Key这样的头nginx 无论怎么配都不转发。 ✅ nginx 默认丢弃请求头里含下划线的字段。真要放行必须显式开underscores_in_headers on;放在http或server层。不过更该做的是把上游的头名改成连字符形式。6.Connection: 和 WebSocket 的upgrade冲突❌ 为了上游 keepalive 写了proxy_set_header Connection ;同一台机器上的 WebSocket 路径握手一直返回 400。 ✅ 用map $http_upgrade $connection_upgrade分流或者给 WebSocket 单开一个 location 并写全所有头。7. 想丢掉某条头却写成了不写❌ 以为不在配置里提这个头它就不会被转发。 ✅ 不写等于透传。要移除必须显式赋空值proxy_set_header Proxy ;。8. 用第三方模块的指令却没装模块❌ 抄来more_clear_input_headers Cookie;直接报unknown directive因为这是headers-more-nginx-module提供的默认编译的 nginx 里没有。 ✅ 只用内置指令清 Cookie 用proxy_set_header Cookie ;。要确认手里的 nginx 支持哪些模块用nginx -V看 configure arguments。总结目标写法移除某个请求头proxy_set_header 头名 ;用真实值覆盖客户端伪造值proxy_set_header X-Forwarded-For $remote_addr;完全不转发客户端头proxy_pass_request_headers off;上游长连接复用proxy_http_version 1.1;proxy_set_header Connection ;WebSocket 升级map $http_upgrade $connection_upgradeConnection $connection_upgrade隐藏上游响应头proxy_hide_header 头名;放行下划线头名underscores_in_headers on;验证实际转发的头裸nc当上游抓原始请求或nginx -T看最终配置核心只有两句话nginx 默认透传客户端请求头要丢就显式赋空值proxy_set_header的继承是全有或全无同一层级要么写全要么一条不写。把这两条记住再配上改完先nginx -t再用nc抓一次真实请求的习惯代理层的头问题基本都能在发布前就拦住。
返回列表