Nginx 作为反向代理时,后端应用服务器看到的客户端 IP 往往是 127.0.0.1 或代理服务器自身的 IP。这个问题直接导致日志审计失效、访问控制策略形同虚设。要解决它,核心在于正确配置 IP 透传头,并在后端正确识别这些头。但仅仅透传还不够,如果直接信任未经清洗的请求头,攻击者可以伪造 X-Forwarded-For 轻松绕过 IP 限制。所以,IP 透传与访问控制必须作为整体方案来设计,而不是两个独立配置。

IP 透传的底层机制与头字段选择

Nginx 实现 IP 透传依赖 HTTP 扩展头。最常用的是 X-Forwarded-For,它记录请求经过的每一跳代理 IP,格式为逗号分隔的列表:客户端 IP, 代理1 IP, 代理2 IP。但 X-Forwarded-For 是非标准字段,不同代理实现有差异。标准化替代方案是 Forwarded 头,RFC 7239 定义,格式为 Forwarded: for=客户端IP;proto=http;by=代理IP。实际部署中,两种头通常同时发送以保证兼容性。Nginx 的 ngx_http_proxy_module 提供了 proxy_set_header 指令来构造这些头。

基础透传配置与变量详解

在 Nginx 的 location 块中,最简配置如下:

location / {
    proxy_pass http://backend;
    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;
}

这里有几个关键变量需要准确理解。$remote_addr 是与 Nginx 直接建立 TCP 连接的客户端 IP,这是最可靠的对端地址,无法伪造。$proxy_add_x_forwarded_for 等价于 $http_x_forwarded_for, $remote_addr,它会将请求中原有的 X-Forwarded-For 值追加上当前客户端 IP,形成完整的代理链。如果请求中本没有该头,则直接设为 $remote_addr。X-Real-IP 通常设为 $remote_addr,用于向后端传递最原始的客户端 IP,但它在多级代理场景下不够准确。对于 Forwarded 头,可以这样设置:

proxy_set_header Forwarded "for=$remote_addr;proto=$scheme;by=$server_addr";

注意 $server_addr 是接受请求的服务器 IP,在多网卡场景下能明确标识流量入口。

多级代理下的透传陷阱

当 Nginx 前面还有 CDN 或负载均衡器时,$remote_addr 会变成上一跳设备的 IP,而非真实客户端。CDN 通常会把客户端 IP 放在 X-Forwarded-For 或自定义头(如 X-Real-IP、Cdn-Src-Ip)中。此时若继续用 $remote_addr 赋值给 X-Real-IP,后端拿到的就是 CDN 节点 IP。正确的做法是利用 real_ip 模块进行 IP 还原。首先确保编译了 --with-http_realip_module,然后在 http 或 server 块中配置:

set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

set_real_ip_from 定义信任的代理 IP 范围,只有来自这些 IP 的请求才会进行 IP 替换。real_ip_header 指定从哪个头取客户端 IP。real_ip_recursive on 是关键:它会从右向左遍历 X-Forwarded-For,排除掉所有信任 IP 后,将最右边的非信任 IP 作为 $remote_addr。例如请求经过 CDN 和内部网关,X-Forwarded-For 为 客户端IP, CDN IP, 网关IP。若信任 CDN IP 和网关 IP,开启递归后 $remote_addr 会被正确替换为客户端IP。关闭递归则只会替换为最右边非信任 IP,可能导致取到 CDN IP。

访问控制的基础实现

IP 透传解决后,访问控制才有意义。Nginx 的 ngx_http_access_module 提供 allow 和 deny 指令,基于 $remote_addr 进行判断。配置示例:

location /admin {
    allow 192.168.1.0/24;
    allow 10.0.0.0/8;
    deny all;
}

如果 real_ip 模块已正确还原客户端 IP,这里的 allow 和 deny 就能针对真实客户端生效。但 access 模块仅支持 IP 段匹配,无法处理复杂逻辑。更灵活的方式是结合 if 指令和 map 映射,或者使用 rewrite 模块返回状态码。例如基于 X-Forwarded-For 中的第一个 IP(原始客户端)做控制:

if ($http_x_forwarded_for !~ "^192\.168\.1\.\d+") {
    return 403;
}

这种写法存在安全风险,因为 $http_x_forwarded_for 可被客户端伪造。必须在确保 Nginx 是边界代理、且已清洗该头的前提下使用。更安全的方式是利用 real_ip 模块处理后的 $remote_addr,或者使用 geo 模块。

geo 模块与动态 IP 黑名单

geo 模块能根据客户端 IP 映射变量值,非常适合做地理位置或 IP 库级别的访问控制。配置示例:

geo $block_country {
    default 0;
    1.2.3.0/24 1;
    5.6.7.0/24 1;
}
server {
    if ($block_country) {
        return 403;
    }
}

geo 模块的优势在于支持大量 IP 段而性能不降级,因为它内部使用基数树结构。如果结合 MaxMind GeoIP 数据库,还能实现国家或城市级别的控制。通过 --with-http_geoip_module 编译后:

geoip_country /path/to/GeoIP.dat;
geoip_city /path/to/GeoLiteCity.dat;
map $geoip_country_code $allowed_country {
    default no;
    CN yes;
    US yes;
}
server {
    if ($allowed_country = no) {
        return 403;
    }
}

这样就能只允许特定国家的访问者。注意 GeoIP 数据库需定期更新,否则 IP 归属地变化会导致误判。

防止 IP 伪造的请求头清洗策略

如果 Nginx 直接面向公网,请求中携带的 X-Forwarded-For 头完全是客户端可控的,必须清除或覆盖。在 server 块中,通过 proxy_set_header 赋值时会自动覆盖客户端传来的值,因为 Nginx 默认不转发客户端请求头,除非显式通过 proxy_pass_request_headers on 开启(默认 off)。但为了绝对安全,可以在接收请求阶段就重置这些头:

proxy_set_header X-Forwarded-For "";
proxy_set_header X-Real-IP "";

不过更推荐的做法是利用 real_ip 模块的递归检查,只信任来自上游代理的头。如果 Nginx 是边界节点,就不要配置 set_real_ip_from 和 real_ip_header,这样 $remote_addr 始终是真实客户端 IP,任何客户端伪造的 X-Forwarded-For 都不会影响访问控制判断。

限流与访问控制的结合

IP 透传后,限流模块 limit_req 和 limit_conn 也能准确作用于真实客户端。基于真实 IP 的限流配置:

limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
location /api {
    limit_req zone=api_limit burst=20 nodelay;
    limit_conn conn_limit 5;
    proxy_pass http://backend;
}

$binary_remote_addr 是 $remote_addr 的二进制压缩形式,节省内存。如果 real_ip 模块已还原客户端 IP,这里的限流就直接针对真实用户。对于需要区别对待的接口,可以结合 map 定义不同速率:

map $uri $rate {
    /api/search 20r/s;
    /api/login 5r/s;
    default 10r/s;
}
limit_req_zone $binary_remote_addr zone=dynamic_limit:10m rate=$rate;

这种动态限流需要 Nginx 1.17.0 以上版本支持变量速率。

日志记录与审计追溯

IP 透传配置完成后,必须同步修改日志格式,否则访问日志中记录的仍是代理 IP。在 http 块中自定义 log_format:

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                '$status $body_bytes_sent "$http_referer" '
                '"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;

如果启用了 real_ip 模块,$remote_addr 已经是真实客户端 IP,$http_x_forwarded_for 则保留完整代理链,便于排查代理路径问题。对于安全审计,建议同时记录 $realip_remote_addr(real_ip 模块提供,记录直接连接 Nginx 的 IP)和 $remote_addr,这样既能追溯真实客户端,也能保留网络拓扑信息。

完整方案示例:边界 Nginx 加后端应用

假设 Nginx 直接暴露在公网,后端是应用服务器集群。完整配置如下:

http {
    # 限流区域定义
    limit_req_zone $binary_remote_addr zone=global_limit:10m rate=30r/s;
    limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
    # 日志格式
    log_format detailed '$remote_addr [$time_local] "$request" $status '
                        '$body_bytes_sent "$http_referer" "$http_user_agent" '
                        'rt=$request_time uct="$upstream_connect_time" '
                        'uht="$upstream_header_time" urt="$upstream_response_time"';
    server {
        listen 80;
        server_name example.com;
        access_log /var/log/nginx/access.log detailed;
        # 全局连接限制
        limit_conn conn_per_ip 10;
        location / {
            limit_req zone=global_limit burst=50 nodelay;
            # 透传配置,覆盖客户端伪造头
            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 http://backend_pool;
        }
        # 管理后台严格限制
        location /admin {
            allow 203.0.113.0/24;
            deny all;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_pass http://backend_pool;
        }
    }
}

这套配置中,边界 Nginx 不信任任何外部 X-Forwarded-For,直接用 $remote_addr 构建新的代理链。管理后台通过 allow/deny 做白名单控制,基于真实 IP 判断。后端应用只需读取 X-Real-IP 或 X-Forwarded-For 的第一个 IP 即可获得客户端真实地址。

常见故障排查思路

配置完成后如果后端仍然获取不到真实 IP,按以下顺序排查:首先确认 Nginx 是否编译了 real_ip 模块(nginx -V 查看)。其次检查 set_real_ip_from 是否包含了所有上游代理 IP,漏掉任何一个都会导致 IP 替换失败。第三,real_ip_recursive 的开关直接影响多级代理下的 IP 选择逻辑,务必根据实际代理层数决定。第四,如果后端是 Apache 或 Tomcat,需要对应配置 RemoteIpValve 或 mod_remoteip 来识别 X-Forwarded-For,否则后端日志仍显示 Nginx IP。第五,检查是否存在 WAF 或安全组在中间改写了请求头。最后,通过 curl 模拟带 X-Forwarded-For 的请求直接测试 Nginx 响应,观察日志中 $remote_addr 的变化,能快速定位问题环节。

IP 透传与访问控制是反向代理架构的基础安全组件。正确的配置能让后端应用透明地获取客户端真实 IP,并在此基础上实施精准的访问控制、速率限制和审计追踪。核心原则是:明确 Nginx 在网络拓扑中的位置,严格定义信任边界,清洗不可信来源的请求头,然后基于还原后的真实 IP 执行安全策略。这套方法论在任何多层代理架构中都适用,区别只在于具体指令的调整。