CDN + Nginx 反向代理下,真实 IP 和登录态该按什么顺序排查?

111 次浏览1 条回复

想请教一套适用于以下拓扑的配置排查顺序:

客户端 -> CDN -> Nginx 反向代理 -> Web 应用

客户端访问 CDN 时使用 HTTPS,CDN 回源到 Nginx 可能是 HTTP 或 HTTPS,再由 Nginx 转发给应用。可能观察到的现象包括:

  • 应用日志里的客户端 IP 是 CDN 节点或 Nginx 地址,而不是真实客户端地址;
  • 应用把外部 HTTPS 请求识别成 HTTP,生成错误的跳转地址;
  • 登录响应已设置 Cookie,但后续请求未能维持登录态,表现为重新登录或登录后又回到未登录状态。

目前考虑按下面顺序排查:

  1. 先在 CDN、Nginx 和应用入口分别确认请求头,检查 CDN 是覆盖还是追加 X-Forwarded-For,以及是否传递准确的 X-Forwarded-Proto
  2. 再配置 Nginx 的可信来源范围,只信任 CDN 官方出口网段;核对 real_ip_headerset_real_ip_fromreal_ip_recursive,并确认转发给应用的 X-Forwarded-For 没有被重复追加或直接信任客户端伪造值。
  3. 然后配置应用或框架的 trusted proxy,限定只接受来自 Nginx/CDN 的代理头,并确认代理层数或可信 IP 范围与实际拓扑一致。
  4. 在应用能够稳定识别原始协议为 HTTPS 后,再检查会话 Cookie 的 SecureSameSiteDomainPath。其中跨站登录或回调若需要 SameSite=None,应同时满足 Secure,否则先判断是否可使用 Lax

这个顺序是否合理?尤其想确认两点:如果 CDN 到 Nginx 使用 HTTP,Nginx 是否应保留 CDN 提供的原始 X-Forwarded-Proto,而不是用 $scheme 覆盖;以及真实 IP 恢复后,传给应用的 X-Forwarded-For 应如何避免重复链路和信任范围过宽。希望能给出一套逐层验证的方法或常见配置陷阱。

这个顺序基本合理,但建议先固定信任边界,再看每层解析结果。关键是区分“TCP 直连来源”和“代理头解析后的客户端”:任何转发头都不能自行成为信任依据。

1. 先收紧 CDN -> Nginx 边界

  • 源站防火墙或安全组只允许 CDN 官方公布的回源 IPv4/IPv6 网段访问,应用端口只允许 Nginx 访问;不要让客户端绕过 CDN 直连源站。
  • CDN 边缘应删除客户端自带的真实 IP 头和 X-Forwarded-Proto,再由 CDN 覆盖写入。确认供应商规定的真实 IP 头名称、值的语义,以及它对 X-Forwarded-For 是覆盖还是追加。
  • set_real_ip_from 只放 CDN 官方出口网段,并建立网段更新、nginx -t 和 reload 流程;不要配置成任意来源。优先使用 CDN 的单值真实 IP 头。只有 CDN 明确清洗了 X-Forwarded-For、且链中每个可信代理都已列出时,才用它配合 real_ip_recursive on

可临时增加不含 Cookie/Authorization 的诊断日志:

log_format trust_chain '$request_id peer=$realip_remote_addr client=$remote_addr '
                       'xff_in="$http_x_forwarded_for" proto_in="$http_x_forwarded_proto"';
access_log /var/log/nginx/trust-chain.log trust_chain;

启用 realip 后,逐请求核对:$realip_remote_addr 应是实际连接 Nginx 的 CDN 节点且落在官方网段内;$remote_addr 应是经可信 CDN 头恢复出的客户端地址。两者不能只看一个。若使用供应商单值头,配置骨架是:

set_real_ip_from <CDN_OFFICIAL_CIDR>;
real_ip_header <CDN_CANONICAL_REAL_IP_HEADER>;
real_ip_recursive off;

2. 再规范 Nginx -> 应用的头

Nginx 已恢复客户端地址后,若应用只需要最终客户端 IP,最清晰的做法是由 Nginx覆盖输出:

proxy_set_header X-Real-IP         $remote_addr;
proxy_set_header X-Forwarded-For   $remote_addr;
proxy_set_header X-Forwarded-Proto $trusted_external_proto;

这里不直接用 $proxy_add_x_forwarded_for,因为入站链可能已含客户端地址,而 realip 又把 $remote_addr 改成了客户端地址,继续追加容易得到重复值。若确实要保留完整审计链,应先定义每一段的生成者和清洗规则,再显式重建;CDN 节点地址可保留在 Nginx 的 $realip_remote_addr 日志中,不必交给应用自行猜链。

X-Forwarded-Proto 必须来自可信 CDN:

  • 外部入口只提供 HTTPS 时,可由 Nginx固定写成 https
  • 同时支持 HTTP/HTTPS 时,只接受 CDN 从外部连接生成的 httphttps,拒绝其他值,再规范化写给应用。
  • CDN 到 Nginx 若是 HTTP,$scheme 只表示这一段回源协议,会错误地得到 http;此时不能用 $scheme 覆盖外部协议。
  • 上述传递成立的前提仍是实际连接来源已由源站网络边界确认为 CDN。不要从任意来源原样透传客户端提交的 X-Forwarded-Proto

3. 应用只信任直接代理

应用或框架的 trusted proxy 应限定为 Nginx 的实际私网地址或最小 CIDR,并确保应用端口不能被其他来源访问。优先按明确地址信任,不要使用全局信任,也不要仅凭固定“代理层数”处理一个可能存在绕行路径的入口。应用侧同时记录:socket peer、框架解析出的客户端 IP、解析后的 scheme、Host 以及收到的规范化代理头。通过标准应是:socket peer 始终为 Nginx,客户端 IP 与 Nginx 的 $remote_addr 一致,scheme 与用户外部访问协议一致。

先用浏览器 Network/Application(或同等工具)确认登录响应是否收到 Set-Cookie、浏览器是否拒收,以及下一跳请求是否实际带出 Cookie:

  1. Secure:外部为 HTTPS 且应用已正确识别 HTTPS 后再核对;若 scheme 仍解析成 HTTP,框架可能不发安全 Cookie或生成错误跳转。
  2. SameSite:普通同站登录优先核对 Lax 是否足够;确有跨站回调或嵌入需求才使用 None,并同时设置 Secure
  3. Domain:优先省略以得到 host-only Cookie;若显式设置,必须与实际访问主机匹配,不能带协议或端口,并检查登录前后是否发生了主机名跳转。
  4. Path:通常应为 /;确认后续接口路径落在其覆盖范围内,并排除同名 Cookie 在不同 Domain/Path 下互相干扰。

5. 做三组可重复核对

# 正常经 CDN 请求,使用同一个请求 ID 对齐 CDN、Nginx、应用日志
curl -sS -D - -o /dev/null https://PUBLIC_HOST/check

# 经 CDN 注入伪造值;最终客户端 IP/协议不应受这两个值控制
curl -sS -D - -o /dev/null https://PUBLIC_HOST/check \
  -H 'X-Forwarded-For: 198.51.100.77' \
  -H 'X-Forwarded-Proto: http'

# 尝试绕过 CDN 直达源站;网络边界应拒绝该路径
curl -sS -D - -o /dev/null --resolve PUBLIC_HOST:443:ORIGIN_IP https://PUBLIC_HOST/check

不要预设这些测试会通过;以三层日志和浏览器 Cookie 拒收原因逐项确认。最终边界应是:CDN 清洗客户端头,Nginx 只接受官方 CDN 来源并覆盖发往应用的头,应用只信任直接连接它的 Nginx。这样即使客户端主动提交转发头,也无法跨过任一层的信任边界。