nginx 反代后后端只看到 127.0.0.1:三层信任链怎么补

举报
逍遥渝枫 发表于 2026/11/08 08:44:51 2026/11/08
【摘要】 反向代理后面,应用拿到的客户端地址经常退化成 127.0.0.1 或上一跳的内网地址,按 IP 做的限流、风控、审计会一起失准。本文用一组在 Ubuntu 22.04 + nginx 1.18.0 上跑过的对照实验,把问题拆成「代理有没有把地址传下去、nginx 认不认上一跳传来的地址、应用信不信这个头」三层,逐层给出配置、验证命令,以及一条容易被忽略的伪造路径,最后附一份八行的现象对照表。

现象:地址退化了,但服务是好的

业务从单机直连改成 nginx 反代之后,功能一切正常,只有一类东西开始不对:凡是跟客户端地址相关的逻辑,全部失灵。应用日志里一整列 127.0.0.1;按 IP 做的登录失败限流再也拦不住人,因为所有请求看起来都来自同一个地址;审计里记录的操作来源全是内网网段;风控规则跑出来的是一堆无意义的数据。

大部分人会去搜「nginx 获取真实 IP」,然后照着抄两行 proxy_set_header 就结束了。但如果入口不止一层 nginx,或者入口本身是云上的负载均衡,只改这两行通常修不好,因为这里其实有三条独立的信任链,任何一条断了,地址都会退回去:

  1. 代理层有没有把地址传给上游:默认情况下 nginx 会原样转发客户端自己带来的头,但不会追加自己的判断,所以上游可能什么都拿不到,也可能拿到一个客户端自己编的值。
  2. nginx 自己认不认上一跳传来的地址:不做处理时 $remote_addr 只等于 TCP 对端。上游如果是负载均衡或另一台 nginx,这个值就是上一跳的地址,不是用户的。
  3. 应用层信不信这个头:应用如果把 X-Forwarded-For 的第一段当真,等于把判断权交给了客户端。

下面这组实验是在一台 Ubuntu 22.04 云主机上跑的,nginx 1.18.0(Ubuntu 官方包),四个 server 块全部只绑 127.0.0.1,用临时配置启动,不碰 /etc/nginx,跑完即退出。所有 IP 都是文档保留网段,只做占位。

第一层:代理到底往下传了什么

先说一个反直觉的实测结果。很多人以为「nginx 默认不传 X-Forwarded-For,所以后端拿不到」,实际情况是:nginx 默认会把客户端原始请求里的头原样透传给上游,只是不追加自己的判断。搭一个只回显请求头的后端,再配一个什么都不设置的 proxy,结果是这样:

# 后端只做一件事:把收到的头原样吐回来
curl -s -H 'X-Forwarded-For: 203.0.113.9' 127.0.0.1:8083/a
# {"path": "/a", "x_forwarded_for": "203.0.113.9", "x_real_ip": null,
#  "host": "127.0.0.1:8099", "peer": "127.0.0.1"}

也就是说,客户端自己伪造的 203.0.113.9 一路直达后端,中间没有被清洗。这时候后端看到的「客户端地址」完全是攻击者填的。补上头之后行为才对:

curl -s -H 'X-Forwarded-For: 203.0.113.9' 127.0.0.1:8083/b
# {"path": "/b", "x_forwarded_for": "203.0.113.9, 127.0.0.1",
#  "x_real_ip": "127.0.0.1", "host": "127.0.0.1", "peer": "127.0.0.1"}

注意 X-Forwarded-For 变成了 203.0.113.9, 127.0.0.1:$proxy_add_x_forwarded_for 的语义是「保留客户端原有的值,把 TCP 对端追加在末尾」。它不清洗,只追加。这一条决定了后端绝不能取第一段。

配置就三行,但 Host 那行经常被漏掉,漏掉时上游会看到 127.0.0.1:8099 这种内部地址,如果上游要用 Host 做站点判断或拼回调地址,会出另一类问题:

location / {
    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_pass 127.0.0.1:8099;
}

第二层:nginx 的 $remote_addr 是不是用户

X-Real-IP 传的是 $remote_addr,那 $remote_addr 到底是什么?在没有任何 realip 配置时,它就是 TCP 对端的地址。入口是多层代理时,这个值是上一跳的地址,于是「真实 IP」被忠实地传成了 127.0.0.1 或 10.x 内网地址。

要让 nginx 把上一跳传来的地址当成客户端地址,得用 http_realip_module。同一个请求,一个 server 块不配,另一个配上:

# 不启用 realip
curl -s -H 'X-Forwarded-For: 203.0.113.9' 127.0.0.1:8081/
# A addr=127.0.0.1 realip_src=127.0.0.1 xff=203.0.113.9

# 启用 realip:set_real_ip_from 127.0.0.1; real_ip_header X-Forwarded-For; real_ip_recursive on;
curl -s -H 'X-Forwarded-For: 203.0.113.9' 127.0.0.1:8082/
# B addr=203.0.113.9 realip_src=127.0.0.1 xff=203.0.113.9

$remote_addr 从 127.0.0.1 变成了 203.0.113.9,而 $realip_remote_addr 保留了真实的 TCP 对端。这两个变量的差值,就是判断「这条记录有没有被伪造」的依据,日志里应该同时记下来。

配置写法:

# 只信任来自这些来源的头,来源要写具体网段
set_real_ip_from  127.0.0.1;
set_real_ip_from  10.0.0.0/8;
real_ip_header    X-Forwarded-For;
real_ip_recursive on;

real_ip_recursive on 的行为值得单独说,因为它和直觉相反。让客户端带一条两跳的伪造链:

curl -s -H 'X-Forwarded-For: 198.51.100.7, 203.0.113.9' 127.0.0.1:8082/
# B addr=203.0.113.9 realip_src=127.0.0.1 xff=198.51.100.7, 203.0.113.9

开启递归后,nginx 从右往左走,跳过所有被信任的地址,停在第一个不可信地址上,所以取到的是 203.0.113.9,而不是最左边那个 198.51.100.7。最左边的值永远是客户端自己写的,不可信。这也是为什么「取 XFF 最后一段」在单层代理下是对的,但多层代理下要看信任网段来算,不能靠位置。

set_real_ip_from 千万别写成 0.0.0.0/0。那等于宣布所有来源都可信,任何人都能用一个头把自己伪装成任意地址,比不配还糟——不配至少日志里记的是真实 TCP 对端。如果入口是云上的负载均衡,这里要填的是负载均衡回源时使用的地址段(在控制台里可以查到),而不是用户网段。

还有一个前提:模块得编译进去。Ubuntu 的官方包带了,自己编的镜像不一定:

nginx -V 2>&1 | tr ' ' '\n' | grep realip
# --with-http_realip_module

没有这个输出,上面所有配置都会在 nginx -t 时报 unknown directive,只能换包或重编。这一点在 22.04 的官方源上验证过,其他发行版需要自己确认。

第三层:应用到底该信谁

前两层修完,X-Real-IP 和 X-Forwarded-For 都对了,第三层才轮到应用。这里最常见的错误是「一律信任这个头」。只要后端端口对外可达——容器映射到 0.0.0.0、或者安全组多开了一个端口——别人就能绕过 nginx 直接连后端,自己填一个头。

所以应用侧的规则是:只有请求来自已知代理地址时,才采信代理头;否则用 TCP 对端地址。

以 Node 为例,trust proxy 的设置方式决定了取哪个值:

# 只信任本机与内网代理;数字表示"信任前几跳",true 表示全信(不要用)
app.set('trust proxy', 'loopback, 10.0.0.0/8');
# 取客户端地址时用 req.ip,不要把 req.headers['x-forwarded-for'] 的第一段当真

trust proxy 设成 true 是最省事也最危险的写法,它等于相信任何来源的头。设成具体网段后,Express 会按与 nginx 相同的逻辑从右往左跳过可信地址,得到的才是可用的地址。

配套的动作是收口上游的暴露面,让「绕过 nginx 直连后端」这条路走不通:

# 上游只绑本机,不从 0.0.0.0 监听
ss -lntp | grep -E '8080|8099'      # 期望看到 127.0.0.1:端口,而不是 0.0.0.0:端口
ufw status numbered                  # 对外只留 22/80/443,应用端口不放行

用访问日志自查,别靠猜

排查这类问题时,最快的办法不是读应用代码,而是让 nginx 自己的日志把三个值同时打出来:$remote_addr(经过 realip 计算后的地址)、$realip_remote_addr(实际 TCP 对端)、$http_x_forwarded_for(客户端带来的原始链)。三者一致说明链路干净,不一致就说明中间有代理或伪造。

log_format trace 'addr=$remote_addr realip_src=$realip_remote_addr '
                 'xff="$http_x_forwarded_for" ua="$http_user_agent"';
access_log /var/log/nginx/trace.log trace;

实验里四条请求的日志是这样的,一眼能看出哪条是多跳伪造:

addr=203.0.113.9 realip_src=127.0.0.1 xff="198.51.100.7, 203.0.113.9" ua="curl/7.81.0"
addr=127.0.0.1  realip_src=127.0.0.1 xff="203.0.113.9"                 ua="curl/7.81.0"

排查顺序建议固定成三步,从外往里,每一步都能独立证伪:

# 1 直连 nginx,看它算出来的地址对不对(带一个已知的假 XFF)
curl -s -H 'X-Forwarded-For: 203.0.113.9' 127.0.0.1:8082/
# 2 绕过 nginx 直连后端,看后端到底收到什么
curl -s 127.0.0.1:8099/echo
# 3 对比两边:nginx 对了而应用不对,问题在应用;两边都不对,问题在 realip

常见现象对照

现象 原因 处理
应用日志里全是 127.0.0.1 代理没传头,或应用读的是 TCP 对端 加 proxy_set_header X-Real-IP,应用改读 req.ip
地址是 10.x 或 172.x 内网段 入口有负载均衡,nginx 未启用 realip 配 set_real_ip_from + real_ip_header,来源填负载均衡回源网段
地址看起来正常,但偶尔出现陌生公网 IP 客户端伪造 XFF,被原样透传 后端只取可信代理追加的那一段;日志同时记 $realip_remote_addr
nginx -t 报 unknown directive “set_real_ip_from” 模块未编译进 nginx nginx -V 确认,换带 realip 的包或重编
Host 变成 127.0.0.1:端口 漏了 proxy_set_header Host $host 补上,或显式写业务域名
限流规则形同虚设 限流键用的是真实 TCP 对端(同一个 nginx) 限流键改用 realip 后的地址,或直接用 limit_req_zone $binary_remote_addr 放在最外层入口
客户端地址全变成负载均衡的健康检查 IP 健康检查请求也被算进业务统计 按 UA 或专用路径把探活流量单独记录
应用侧 trust proxy 设为 true 后地址可任意伪造 信任范围过大 改成具体网段或跳数

收口清单

三层修完,剩下的是一致性问题:同一个地址在所有地方必须是同一个含义,否则排查时会互相矛盾。

  • 入口只有一层时,set_real_ip_from 可以不写,但 proxy_set_header 一定要写。
  • 入口有多层时,每一层都要配,且信任网段要逐层收紧,最外层只信负载均衡回源段。
  • 上游端口只绑 127.0.0.1,对外只经 nginx,这一条比任何头部配置都重要。
  • 日志里固定记三个值,出问题时先看日志再看代码。
  • 限流、风控、审计统一取同一个来源字段,不要一处取 req.ip、另一处取 XFF 第一段。

写这篇文章时唯一想强调的是:真实 IP 不是一个配置项,而是一条链。只改其中一层,问题会以另一种形式回来——最常见的是「nginx 日志对了,应用还是 127.0.0.1」,那其实是第一层和第三层没对齐,跟 realip 没关系。上面的实验数据都在 22.04 + nginx 1.18.0 上得到,其他发行版与更高版本的 nginx 行为应当一致,但模块编译情况和默认包内容需要各自确认。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。