跳转到帖子

咖啡馆实战:网站接入 CDN 与反向代理后的源站暴露、真实 IP 和日志链路排查

精选回复

发布于

背景

网站接入 CDN、云加速器或反向代理后,常见架构是“客户端 → CDN/WAF → 负载均衡 → Nginx 源站”。这类改造能提升访问速度和抗压能力,但也容易引入三个实际问题:源站仍可被直接访问、客户端 IP 可通过请求头伪造、边缘与源站日志无法关联。

下面以一次授权测试中的典型场景为例:业务域名已经接入 CDN,管理后台按客户端 IP 做访问限制,源站 Nginx 使用 X-Forwarded-For 获取真实地址。测试发现,修改请求头后可以绕过后台限制,同时使用历史解析记录中的源站地址能够直接访问站点,导致 CDN 上的限速和 WAF 规则失效。

以下命令中的域名、地址均为示例。验证应在明确授权的业务、测试源地址和时间窗口内进行,避免对生产缓存及访问控制造成影响。

问题定位

排查时不要先盯着单条 WAF 告警,应先画清实际链路,并确认每一跳由谁写入、追加或信任转发头。重点核对以下内容:

  • DNS 当前解析、历史地址及源站证书是否暴露真实入口。
  • 源站 80、443 端口是否允许任意公网地址连接。
  • Nginx 是否把 0.0.0.0/0 配置成可信代理。
  • CDN、负载均衡是否覆盖客户端传入的 X-Forwarded-For、X-Real-IP。
  • 源站访问控制使用的是 TCP 对端地址,还是经过 realip 模块重写后的地址。
  • 边缘层和源站是否保留统一的请求 ID。

先在源站确认监听情况、编译模块和最终生效配置。nginx.conf 中看起来正确,并不代表 include 后的完整配置正确。

ss -lntp | grep -E ':80|:443'
nginx -V 2>&1 | grep -o http_realip_module
nginx -T 2>&1 | grep -nE 'listen|server_name|real_ip|set_real_ip_from|X-Forwarded-For|allow|deny'
nft list ruleset

技术分析一:错误信任转发头

高风险配置通常类似下面这样:

set_real_ip_from 0.0.0.0/0;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

这相当于把所有访问者都当成可信代理。攻击者直接连接源站或经过不会清洗请求头的上游时,可以自行构造 X-Forwarded-For。Nginx 的 $remote_addr 经过 realip 模块处理后可能变成伪造地址,基于它实现的后台白名单、限速、审计和封禁都会失去可靠性。

需要区分两个变量:

  • $remote_addr:启用 realip 后,通常表示解析得到的客户端地址。
  • $realip_remote_addr:保存建立 TCP 连接的原始对端地址,适合确认请求实际上来自哪个 CDN 节点或负载均衡。

在“客户端 → CDN → 内部负载均衡 → Nginx”的多跳架构中,源站只能信任已确认的 CDN 出口网段和内部负载均衡网段。不能为了兼容多跳直接信任全网。

技术分析二:源站直连绕过

即使真实 IP 配置正确,只要源站公网端口对任意地址开放,访问者仍可能通过源站 IP 绕开 CDN。仅修改 Host 头或使用本地解析即可验证这一点:

# 仅从已登记的授权测试地址执行
curl -skI --resolve site.example:443:192.0.2.10 

# 对比正常 CDN 路径
curl -skI 

如果两个请求都返回相同业务内容,说明源站缺少网络层或应用层入口限制。需要进一步比较响应头、证书、缓存标记和日志中的 TCP 对端地址,避免把 CDN 回源地址误判为真实源站。

默认虚拟主机返回 444 可以减少直接扫 IP 时的信息暴露,但不能替代源站防火墙。只要访问者知道正确的域名并携带对应 SNI 和 Host,仍可能命中正式站点。

操作步骤:建立可关联的日志链路

先补齐日志字段,再做修复验证。建议源站同时记录原始对端、解析后的客户端地址、转发链、请求 ID、Host、上游耗时和状态码。

log_format origin_trace
    '$time_iso8601 rid=$http_x_request_id '
    'peer=$realip_remote_addr client=$remote_addr '
    'xff="$http_x_forwarded_for" host="$host" '
    'request="$request" status=$status bytes=$body_bytes_sent '
    'rt=$request_time ua="$http_user_agent"';

access_log /var/log/nginx/origin_trace.log origin_trace;

边缘代理应覆盖而不是盲目信任客户端提交的请求 ID。自建 Nginx 边缘层可以这样传递:

proxy_set_header X-Request-ID $request_id;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;

若使用云 CDN,应确认控制台是否支持覆盖 X-Request-ID 或使用厂商提供的唯一请求标识。不要直接把外部传入的任意字符串当成审计主键,否则可能出现日志混淆。

验证时发送一个唯一请求,然后在边缘和源站按请求 ID 检索:

RID=test-20250308-001
curl -sk  \
  -H "X-Request-ID: ${RID}" \
  -H 'X-Forwarded-For: 198.51.100.77' \
  -o /dev/null -D -

grep 'rid=test-20250308-001' /var/log/nginx/origin_trace.log

如果边缘层正确覆盖请求 ID,上述客户端自定义值不应原样成为可信关联字段。若测试环境暂时允许透传,则至少要结合时间、URI、边缘日志和 TCP 对端共同判断。

操作步骤:正确配置真实 IP

下面是源站直接接收 CDN 回源流量时的示例。示例网段必须替换为 CDN 官方公布并经过核验的出口网段:

# 只信任真实存在的 CDN 回源地址
set_real_ip_from 203.0.113.0/24;
set_real_ip_from 2001:db8:100::/48;

real_ip_header X-Forwarded-For;
real_ip_recursive on;

如果源站前还有内部负载均衡,需要同时信任负载均衡网段,并确认它不会接受任意公网流量:

set_real_ip_from 10.20.0.0/16;
set_real_ip_from 203.0.113.0/24;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

不要把 PROXY Protocol 和普通 X-Forwarded-For 混在一起试错。只有负载均衡明确发送 PROXY Protocol 时,源站才能使用 listen 443 ssl proxy_protocol 和 real_ip_header proxy_protocol;否则会直接导致 TLS 请求失败。配置修改后应先执行 nginx -t,再灰度重载。

nginx -t && nginx -s reload

操作步骤:限制源站入口

优先在安全组、防火墙或负载均衡监听器上限制源站 80、443,只允许 CDN 回源网段和必要的健康检查地址。应用层限制可以作为补充,但不应是唯一防线。

如果短期内无法调整网络策略,可以基于原始 TCP 对端增加临时保护。这里使用 $realip_remote_addr,而不是可能已被改写的 $remote_addr:

geo $realip_remote_addr $edge_peer_ok {
    default             0;
    203.0.113.0/24      1;
    2001:db8:100::/48   1;
    10.20.0.0/16        1;
}

server {
    listen 443 ssl;
    server_name site.example;

    if ($edge_peer_ok = 0) { return 403; }

    location / {
        proxy_pass 
    }
}

这里的 if 只执行 return,行为相对明确。长期方案仍应放到网络层,避免 Nginx 配置遗漏某个虚拟主机或新端口。

同时配置默认虚拟主机,减少错误 Host 和直接扫 IP 的信息暴露:

server {
    listen 80 default_server;
    server_name _;
    return 444;
}

server {
    listen 443 ssl default_server;
    server_name _;
    ssl_certificate     /etc/nginx/tls/default.crt;
    ssl_certificate_key /etc/nginx/tls/default.key;
    return 444;
}

修复后的验证方法

  • 从正常公网访问业务域名,确认 CDN 路径可用,源站日志中的 peer 为 CDN 或负载均衡地址。
  • 携带伪造 X-Forwarded-For 请求,确认 client 不会被错误解析为攻击者指定的白名单地址。
  • 从授权测试地址直连源站,预期在网络层超时或被拒绝,而不是返回正式业务页面。
  • 检查后台白名单、Nginx limit_req、应用审计使用的地址字段是否一致。
  • 抽取同一请求 ID,对比 CDN、负载均衡、Nginx 和应用日志,确认状态码与时间线能够闭环。
  • 检查 IPv6。很多环境只限制了 IPv4 源站地址,却保留了可直连的 AAAA 记录或 IPv6 安全组规则。

风险与修复建议

  • 最高优先级:在网络层关闭源站公网直连,仅允许 CDN、负载均衡和健康检查网段。
  • 最高优先级:删除 set_real_ip_from 0.0.0.0/0,只信任经过核验的代理地址。
  • 高优先级:由最外层可信代理覆盖客户端可控的真实 IP 头和请求 ID。
  • 高优先级:管理后台不要只依赖源 IP,至少叠加身份认证、MFA 和独立访问入口。
  • 中优先级:同步维护 CDN IPv4、IPv6 网段。自动更新脚本应先校验数据格式和变更范围,不要直接 curl 后覆盖生产配置。
  • 中优先级:保留 peer、client、X-Forwarded-For 和请求 ID,设置合理的日志留存及时间同步。
  • 中优先级:检查缓存键是否包含正确的 Host、URI 和必要查询参数,避免不同站点或权限响应共享缓存。

总结

CDN 和反向代理上线后,真正需要关注的不是“页面是否加速”,而是入口边界和信任链是否发生变化。源站防火墙决定谁能连接,realip 配置决定谁有资格声明客户端地址,日志字段决定事后能否还原链路。这三部分缺少任何一项,都可能让 WAF、限速、后台白名单和审计产生错误结论。

比较稳妥的处理顺序是:先补日志并确认实际链路,再收紧可信代理范围,最后封闭源站直连并完成旁路验证。这样既能减少业务中断,也能让每一步修复都有明确证据。

网络流量与边界分析示意图
网络请求、日志与边界流量分析示意
服务器与云环境安全示意图
服务器、云资产与权限边界梳理示意

这类问题建议把“源站是否能被绕过”和“日志里的客户端 IP 是否可信”分开查。接了 CDN 不等于源站已隔离,日志里出现公网 IP 也不代表取值正确。

先验证源站入口是否真的收口。在自有或授权环境下,从非 CDN 网络直连源站,保留域名的 Host 和 TLS SNI:

curl -sv --connect-timeout 5 \
  --resolve ' \
   -o /dev/null

如果仍能拿到业务页面,说明至少这个入口可以绕过 CDN。除了 IPv4,还要检查 IPv6、安全组、备用端口,以及直接暴露的反向代理节点。纯 CDN 回源架构下,公网入口只允许 CDN 官方回源网段;若源站前还有独立反代,源站则只允许反代访问。需要更强约束时加回源 mTLS,不能只靠隐藏 IP 或检查 Host。

真实 IP 要从第一个可信入口开始处理,不能直接相信请求自带的 X-Forwarded-For。以 CDN → Nginx 反代 → 源站为例,反代只信任 CDN 网段,解析完成后向源站覆盖写入客户端 IP:

# 放在 CDN 后面的 Nginx;网段必须换成服务商官方列表并持续更新
set_real_ip_from 192.0.2.0/24;  # 仅为文档示例,不能照抄上线
real_ip_header X-Forwarded-For;
real_ip_recursive on;

location / {
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Request-ID $request_id;
    proxy_pass 
}

这里用覆盖而不是追加,是为了让源站接收到一条明确、受控的客户端地址;完整来路留在入口日志里。前提是 CDN 确实会按官方规则清洗或追加该头;如果服务商提供专用客户端 IP 头,应按其文档配置。源站只信任反代地址,业务代码不要再自行取 XFF 最左项。

日志同时保留“解析后的客户端”和“实际连接对端”。启用 realip 后,$remote_addr 已可能被改写,连接对端要看 $realip_remote_addr:

log_format chain escape=json
  '{"time":"$time_iso8601","rid":"$request_id",'
  '"client":"$remote_addr","peer":"$realip_remote_addr",'
  '"xff":"$http_x_forwarded_for","status":"$status",'
  '"upstream":"$upstream_addr","urt":"$upstream_response_time"}';

access_log /var/log/nginx/chain.log chain;

最后补一次伪造头测试:经过 CDN 请求时手动加 X-Forwarded-For: 1.2.3.4,确认入口解析出的客户端及源站日志不会被改成这个值。再用同一个入口生成的请求 ID 串起反代和应用日志;源站应记录收到的 ID,而不是重新生成,否则排查时很容易把两次请求对错。

创建帐户或登录后发表意见

最近浏览 0

  • 没有会员查看此页面。