适用场景
Nginx 获取真实客户端 IP 是接入 CDN、WAF 或负载均衡之后必然要处理的第一件事。流量路径变为”客户端 → 边缘节点 → 源站 Nginx”后,源站看到的 TCP 源地址永远是边缘节点的地址,会导致三类问题同时出现:
- 日志失真:access.log 里全是 CDN 节点 IP,攻击溯源、UV 统计、地域分析全部失效。
- 限流误伤:
limit_req、limit_conn按边缘 IP 计数,少量 CDN 节点会把阈值占满,正常用户被大面积拒绝。 - 封禁误封:fail2ban 依据日志封禁,最终把 CDN 回源节点整段封掉,网站全站不可用。
凡是”前面套了一层代理”的架构,都需要通过 ngx_http_realip_module 还原真实 IP,同时防止客户端伪造 X-Forwarded-For 绕过限流与封禁。
前置条件
- Nginx 1.13.0 及以上版本(
real_ip_recursive从 1.13.0 起支持)。 - Nginx 编译时已启用
--with-http_realip_module(官方预编译包与大多数发行版默认包含)。 - 已获取上游 CDN / WAF 的回源网段列表,不能只写单个 IP。
- CDN 侧已开启”携带真实客户端 IP 回源”,即回源请求中带有
X-Forwarded-For或X-Real-IP。 - 源站已配置 CDN 回源 IP 白名单,只允许 CDN 节点访问(详见同站《Nginx 源站防护实战》)。
原理说明
X-Forwarded-For 的格式是一个逗号分隔列表:客户端IP, 代理1IP, 代理2IP。每经过一层代理,代理会把上一跳的地址追加到列表末尾。关键问题在于:客户端可以自行伪造这个请求头。如果源站无条件信任它,攻击者发送 X-Forwarded-For: 1.1.1.1 就能让日志显示任意 IP,也能让限流规则按 1.1.1.1 计数,从而无限绕过节流。
Nginx 的 realip 模块通过三个指令解决这个问题:
- set_real_ip_from:声明”哪些地址是可信代理”。只有来自这些地址的请求,其
X-Forwarded-For才会被采信。 - real_ip_header:指定从哪个请求头读取真实 IP,常用
X-Forwarded-For;使用 Proxy Protocol 时填proxy_protocol。 - real_ip_recursive:
on时从右往左逐层剥离可信代理地址,取最右侧的不可信地址作为客户端 IP;off时直接取列表中最右侧的值。
这里有一个极易混淆的点:real_ip_recursive off 时取的是 最右值,而最右值恰恰是离源站最近的那一跳,通常是可信的 CDN 节点;on 时从右往左剔除可信段,最终落到客户端自己声明的第一个地址——所以只有配合 set_real_ip_from 的严格网段约束,recursive on 才是安全的。机制正确配置后,$remote_addr 会被改写为真实客户端 IP,日志、限流、封禁全部自动跟随,无需改动其他模块。
需要强调的是:X-Real-IP 是单值头部,完全不携带层级信息,一旦源站直接暴露在公网,攻击者可以直接伪造它。因此更稳妥的选择是 X-Forwarded-For 配合网段白名单,或者使用 Proxy Protocol——后者由 TCP 层承载,应用层无法伪造,是可靠性最高的方案。
操作步骤
1. 确认 realip 模块已编译
nginx -V 2>&1 | tr ' ' '\n' | grep -i realip
# 期望输出:--with-http_realip_module
若没有输出,需重新编译 Nginx 或改用发行版自带的 nginx-full 包。缺少该模块时后续指令会直接导致 nginx -t 失败。
2. 配置可信代理网段与真实 IP 还原
新建 /etc/nginx/conf.d/realip.conf(Debian/Ubuntu 路径一致),内容放在 http 层,可被所有 server 继承:
# 上游 CDN / WAF 回源网段(示例,须替换为厂商公布的真实网段)
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 2400:cb00::/32;
# 内网负载均衡 / 反向代理
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
set_real_ip_from 127.0.0.1;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
关键点:set_real_ip_from 必须覆盖全部可能的代理来源。漏写一个网段,来自该网段的请求就会被当作普通客户端,日志里会出现”一部分真实、一部分节点 IP”的混杂现象。云厂商的网段列表会更新,建议每季度核对一次;阿里云、腾讯云等厂商提供 IP 段查询接口与变更公告。
3. 调整日志格式,同时保留原始信息
realip 模块会改写 $remote_addr,但不会改写 $realip_remote_addr(原始 TCP 源地址)与 $http_x_forwarded_for。为了事后可审计,建议在日志中同时记录三者:
log_format realip '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" edge=$realip_remote_addr '
'xff="$http_x_forwarded_for" rt=$request_time';
access_log /var/log/nginx/access.log realip;
error_log /var/log/nginx/error.log warn;
edge=$realip_remote_addr 记录的是实际建立 TCP 连接的边缘节点地址,出现异常时可用它区分”节点被入侵”与”客户端伪造头部”两种情况。xff 字段保留原始头部,便于回溯多层代理链路。
4. 让限流与封禁基于真实 IP
# 按真实客户端 IP 限流($remote_addr 已被 realip 改写)
limit_req_zone $binary_remote_addr zone=req_per_ip:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
server {
listen 443 ssl;
server_name www.example.com;
location / {
limit_req zone=req_per_ip burst=20 nodelay;
limit_conn conn_per_ip 20;
proxy_pass http://backend;
}
# 对敏感接口单独收紧
location /api/login {
limit_req zone=req_per_ip burst=5 nodelay;
proxy_pass http://backend;
}
}
这里必须使用 $binary_remote_addr 而非 $remote_addr 作为 key,前者是二进制定长格式,内存占用约为后者的十分之一。
5. 防止 XFF 伪造绕过(关键一步)
仅做上述配置仍存在缺口:如果源站 IP 可被直接访问(绕过 CDN),攻击者可以在请求中自带 X-Forwarded-For,此时 set_real_ip_from 因源地址不在可信网段而不生效,Nginx 会使用真实 TCP 源地址,安全性得以保留。因此真正的加固由两步构成:
- 源站只允许 CDN 回源网段访问,用 iptables 在入口处封掉非 CDN 流量,让 Nginx 永远只在可信路径上收到请求。
- 清理客户端传入的多余层级。对于多层代理场景,最右的若干跳可信、左侧全是客户端可控内容,采用
real_ip_recursive on剥离全部可信层后得到的就是客户端真实地址,不会取到伪造的中间值。
# 源站入口仅放行 CDN 回源网段(示例网段须替换)
iptables -I INPUT 1 -p tcp --dport 443 -s 173.245.48.0/20 -j ACCEPT
iptables -I INPUT 1 -p tcp --dport 443 -s 103.21.244.0/22 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j DROP
netfilter-persistent save
如需在不改防火墙的前提下做校验,可用 map 对头部做正则合法性检查,把非法值替换为空串,避免脏数据进入日志:
map $http_x_forwarded_for $valid_xff {
default "";
"~^\s*\d{1,3}(\.\d{1,3}){3}(,\s*\d{1,3}(\.\d{1,3}){3})*$" $http_x_forwarded_for;
}
6. 更高可靠性的替代方案:Proxy Protocol
当 CDN 或四层负载均衡支持 Proxy Protocol 时,应优先使用它。Proxy Protocol 在 TCP 连接建立时以明文头部传递真实地址,应用层无法注入或篡改:
stream {
server {
listen 443 proxy_protocol;
proxy_pass 127.0.0.1:8443;
}
}
http {
server {
listen 8443 ssl proxy_protocol;
set_real_ip_from 127.0.0.1;
real_ip_header proxy_protocol;
}
}
注意 listen 指令必须显式带 proxy_protocol 参数,否则 Nginx 会把协议头当业务数据解析,导致 400 错误。使用该方案时须确认 CDN 侧已开启 Proxy Protocol 回源,否则 Nginx 会因收不到协议头而拒绝连接。
配置验证
# 1) 语法检查并平滑重载
nginx -t && nginx -s reload
# 2) 从 CDN 域名访问,观察日志首列是否为真实公网 IP
tail -f /var/log/nginx/access.log
# 3) 伪造头部测试:源站被白名单保护时该请求应被拒绝
curl -I -H 'X-Forwarded-For: 203.0.113.9' https://www.example.com/
# 4) 确认限流 key 已按真实 IP 生效
grep -c 'limiting requests' /var/log/nginx/error.log
# 5) 检查改写的地址是否与 CDN 侧看到的客户端 IP 一致
# 用同一台客户端访问第三方 IP 查询站点做交叉比对
验证标准有三条:日志首列显示客户端公网 IP;edge= 字段显示 CDN 节点 IP;伪造 XFF 从公网直接打源站 IP 时无法进入(连接被防火墙丢弃或返回 403)。
常见问题
FAQ 1:配置了 real_ip 但日志里依然是 CDN 节点 IP
按以下顺序排查:
- set_real_ip_from 网段不全:这是最常见原因。用
edge=$realip_remote_addr看实际连接来源,再与该网段列表比对,缺哪个补哪个。 - real_ip_header 名写错或 CDN 未回传:不同 CDN 可能使用
X-Forwarded-For、X-Real-IP或自定义头(如X-Client-IP)。查看xff=字段是否为空,为空说明头部根本没到源站,属于 CDN 侧配置问题。 - real_ip_recursive 与预期不符:
off取最右值,多层代理下需改为on才能剥离全部可信层。 - 指令放错层级:
set_real_ip_from可以放在http、server、location层,但如果被下游更具体的块覆盖,会只对部分 server 生效。执行nginx -T | grep -n real_ip查看最终生效配置。 - 配置未重载:修改后必须
nginx -t && nginx -s reload,仅systemctl restart之外的任何”看起来生效了”的假象都需排除。
FAQ 2:接入 realip 之后出现大面积误封或限流异常
典型表现是 fail2ban 短时间内封禁大量”看起来不同”的 IP,或正常用户被限流。根因有两类:
- XFF 伪造导致伪造 IP 被计入黑名单:攻击者用随机 XFF 触发规则,污染 fail2ban 的封禁列表。解决方式是只从可信路径采集日志(源站白名单收紧),并在 fail2ban 的正则中提取
edge=字段而非首列 IP 做封禁依据——被伪造的头部不再影响封禁决策,虽然会少封真实攻击者,但不会误封。 - 架构变更后阈值未重算:从”按节点 IP 计数”切换为”按真实客户端 IP 计数”后,单个 IP 的并发与速率均大幅下降,原有阈值可能过于宽松或过于严格。建议先以
log_only模式观察 24 小时的真实分布,再据此设定burst与rate,避免直接上线拦截。
另外注意:使用 real_ip_recursive on 时,如果 CDN 自身也是多层节点(边缘 → 中间层 → 回源),且中间层网段未列入信赖列表,Nginx 会把中间层地址当作客户端 IP。这类架构下应把所有层级的网段一次性补全,或改用 Proxy Protocol 从根本上规避。
FAQ 3:能否直接信任 X-Real-IP 而不配 set_real_ip_from
不能。X-Real-IP 是单值头,不含层级信息,无法判断是否被伪造。若源站存在任何一条绕过 CDN 的访问路径(例如绑定了独立 IP、IPv6 未接入 CDN、旧解析记录未清理),攻击者即可用它伪装任意 IP,日志、限流、地域封禁全部失效。若确实要用,必须同时满足两个条件:源站在网络层只接受 CDN 回源网段访问,且 set_real_ip_from 已列出全部可信网段。
总结
Nginx 真实 IP 还原的本质是”信任边界管理”:set_real_ip_from 定义谁可信,real_ip_header 定义信什么,real_ip_recursive 定义如何在一串不确定的地址中找出可信的那一个。三者缺一,日志就会失真,限流与封禁的判定基准也就随之失效。
落地顺序建议为:先锁定源站入口只允许 CDN 回源,再配置 realip 还原真实 IP,然后调整日志格式保留 edge 与原始 XFF 用于审计,最后重新校准限流与 fail2ban 阈值。对可靠性要求高的场景,直接采用 Proxy Protocol 替代应用层头部,可以从根本上消除伪造面。