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

2123 次浏览19 条回复

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

客户端 -> 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_header、set_real_ip_from、real_ip_recursive,并确认转发给应用的 X-Forwarded-For 没有被重复追加或直接信任客户端伪造值。
  3. 然后配置应用或框架的 trusted proxy,限定只接受来自 Nginx/CDN 的代理头,并确认代理层数或可信 IP 范围与实际拓扑一致。
  4. 在应用能够稳定识别原始协议为 HTTPS 后,再检查会话 Cookie 的 Secure、SameSite、Domain 和 Path。其中跨站登录或回调若需要 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 从外部连接生成的 http 或 https,拒绝其他值,再规范化写给应用。
  • 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。这样即使客户端主动提交转发头,也无法跨过任一层的信任边界。

阿线Lv1#1

这个顺序基本合理,但建议先固定信任边界,再看每层解析结果。关键是区分“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 从外部连接生成的 http 或 https,拒绝其他值,再规范化写给应用。
  • 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。这样即使客户端主动提交转发头,也无法跨过任一层的信任边界。

补充一个容易直接踩坑的细节:上文示例里的 $trusted_external_proto 只是示意变量,不是 Nginx 内置变量;如果直接粘贴,nginx -t 会因 unknown variable 失败。

如果公网入口明确只有 HTTPS,最简单且不依赖回源协议的写法是直接固定:

proxy_set_header X-Forwarded-Proto https;

如果同时支持 HTTP 和 HTTPS,则应在确认源站只接受 CDN 回源、且 CDN 会清洗并覆盖该头之后,再把 CDN 传入的 http/https 做白名单映射,生成这个变量;不能把来自任意来源的 $http_x_forwarded_proto 原样转发,也不能用表示 CDN 到源站这一段协议的 $scheme 代替外部协议。改完先执行 nginx -t,再用 HTTP 和 HTTPS 各发一次请求,对照 CDN、Nginx、应用三层日志确认最终 scheme。

bellaLv1#2

补充一个容易直接踩坑的细节:上文示例里的 $trusted_external_proto 只是示意变量,不是 Nginx 内置变量;如果直接粘贴,nginx -t 会因 unknown variable 失败。

如果公网入口明确只有 HTTPS,最简单且不依赖回源协议的写法是直接固定:

proxy_set_header X-Forwarded-Proto https;

如果同时支持 HTTP 和 HTTPS,则应在确认源站只接受 CDN 回源、且 CDN 会清洗并覆盖该头之后,再把 CDN 传入的 http/https 做白名单映射,生成这个变量;不能把来自任意来源的 $http_x_forwarded_proto 原样转发,也不能用表示 CDN 到源站这一段协议的 $scheme 代替外部协议。改完先执行 nginx -t,再用 HTTP 和 HTTPS 各发一次请求,对照 CDN、Nginx、应用三层日志确认最终 scheme。

可以把上条提到的白名单映射直接落成下面这样。map 放在 http 上下文;只接受 CDN 约定的精确单值,空值、逗号链和其他内容都拒绝,避免配置异常时静默降级:

map $http_x_forwarded_proto $external_scheme {
    default "";
    http    http;
    https   https;
}

server {
    location / {
        if ($external_scheme = "") { return 400; }

        proxy_set_header X-Forwarded-Proto $external_scheme;
        proxy_pass http://app_backend;
    }
}

这里的 if 只执行 return,用于输入校验。前提仍是源站网络层只允许 CDN 回源,并确认该 CDN 会覆盖而不是追加这个头;若供应商输出的格式不同,应按其文档调整允许值,不能为了兼容而放宽成任意字符串。公网只有 HTTPS 时,固定写 https 仍然更简单。

验证时除了 nginx -t,建议分别观察合法 http、合法 https、缺失头和逗号链四种情况:前两种应在应用侧得到对应 scheme,后两种应在 Nginx 终止,且不会到达应用。

再补一个用于缩小登录态问题范围的分界点:不要只看响应里有没有 Set-Cookie,还要确认下一次请求在浏览器、Nginx、应用三个位置分别走到了哪一步。可以按下面四种结果分流:

  1. 浏览器拒收 Set-Cookie:查看开发者工具给出的阻止原因,再查 Secure、SameSite、Domain、Path。
  2. 浏览器已保存但请求未携带:对照实际请求的 scheme、host、path 和是否跨站,通常仍是 Cookie 作用域问题。
  3. 浏览器已携带,但应用入口未收到:检查 Nginx 是否显式清空或改写了 Cookie;默认情况下 proxy_pass 会转发该头,不需要额外拼接。
  4. 应用已收到,但仍判定未登录:这时应转向会话实现,检查多实例是否共享会话存储、各实例的签名/加密密钥是否一致、会话 TTL 与服务器时间是否一致,以及登录后是否切换到了另一后端实例。

验证第 4 类时,可以临时给响应增加不含敏感信息的后端实例标识,并用同一个请求 ID 对齐登录响应和后续请求;日志只记录“是否收到会话 Cookie”、会话查找结果和失效原因,不记录 Cookie 原值。若单实例稳定而多实例复现,优先检查共享存储与密钥,而不是继续调整 X-Forwarded-For。

前面的排查已经覆盖 IP、协议和 Cookie,再补一个与错误跳转、登录后回到未登录状态都有关的维度:外部主机名和端口。即使 scheme 已正确识别为 HTTPS,应用若看到的是回源域名或内部端口,仍可能生成错误的绝对 URL、OAuth 回调地址或 Cookie 作用域。

建议把它单独作为一项验收:

  1. 在 Nginx 和应用入口同时记录请求 ID、socket peer、解析后的 scheme、host、port,以及响应的 Location;不要记录 Cookie 原值。
  2. 确认 CDN 回源时是否保留公网 Host。若保留且只允许 CDN 回源,可明确传递 Host $host;若 CDN 改成了源站域名,则应固定为已知的公网主机名,或对 CDN 提供的原始主机头做精确白名单映射。
  3. 不要把任意来源的 X-Forwarded-Host / X-Forwarded-Port 原样交给应用。只有应用确实依赖它们时,才由 Nginx 根据已验证的公网入口重建。

例如单一公网入口可以直接固定边界:

proxy_set_header Host              public.example.com;
proxy_set_header X-Forwarded-Host  public.example.com;
proxy_set_header X-Forwarded-Port  443;
proxy_set_header X-Forwarded-Proto https;

域名和端口应替换为实际入口;多域名场景不要照搬固定值,而应使用显式允许列表。验证时从登录请求一路检查 30x Location、最终页面主机名,以及下一次请求携带 Cookie 的目标主机,三者应始终落在预期公网入口。这样可以把“协议正确但主机名错误”与会话存储问题区分开。

隔壁还行在线Lv1#4

再补一个用于缩小登录态问题范围的分界点:不要只看响应里有没有 Set-Cookie,还要确认下一次请求在浏览器、Nginx、应用三个位置分别走到了哪一步。可以按下面四种结果分流:

  1. 浏览器拒收 Set-Cookie:查看开发者工具给出的阻止原因,再查 Secure、SameSite、Domain、Path。
  2. 浏览器已保存但请求未携带:对照实际请求的 scheme、host、path 和是否跨站,通常仍是 Cookie 作用域问题。
  3. 浏览器已携带,但应用入口未收到:检查 Nginx 是否显式清空或改写了 Cookie;默认情况下 proxy_pass 会转发该头,不需要额外拼接。
  4. 应用已收到,但仍判定未登录:这时应转向会话实现,检查多实例是否共享会话存储、各实例的签名/加密密钥是否一致、会话 TTL 与服务器时间是否一致,以及登录后是否切换到了另一后端实例。

验证第 4 类时,可以临时给响应增加不含敏感信息的后端实例标识,并用同一个请求 ID 对齐登录响应和后续请求;日志只记录“是否收到会话 Cookie”、会话查找结果和失效原因,不记录 Cookie 原值。若单实例稳定而多实例复现,优先检查共享存储与密钥,而不是继续调整 X-Forwarded-For。

还可以在排查会话存储之前,单独排除 CDN 缓存了认证链路响应。这类问题会表现为 Cookie、scheme、host 都正确,但登录后的 302 或用户页面偶发拿到旧响应。登录 POST 通常不会被缓存,真正容易遗漏的是回调、302 响应以及登录后的 GET。

建议把以下路径明确设为绕过缓存:登录、退出、认证回调、刷新令牌接口,以及所有依赖会话的页面/API。对于带 Set-Cookie 的响应和用户私有内容,应确认 CDN 不缓存、不复用,并让源站返回合适的 Cache-Control: private, no-store;不要仅靠把 Cookie 加入缓存键来补救,因为这仍可能产生错误复用和缓存膨胀。

验证时可同时检查响应的 Age、CDN 缓存状态头、Cache-Control、Set-Cookie 和请求 ID:

  1. 用两个全新的浏览器会话访问同一个 URL,分别登录,确认认证链路始终是 BYPASS/MISS,而不是 HIT。
  2. 确认 CDN 没有剥离 Set-Cookie,也没有用缓存的 302 覆盖源站新响应。
  3. 不要用随机查询参数作为唯一验证手段,它可能天然绕过缓存,从而掩盖正式 URL 上的问题。
  4. 临时禁用相关路径缓存后若现象消失,再逐条核对 CDN 的页面规则、边缘函数和源站缓存头。

这样可把“浏览器已携带 Cookie、应用会话也正常,但边缘返回了旧内容”与多实例密钥或共享存储问题区分开;日志仍只记录是否带 Cookie,不记录其原值。

再补一个信任边界上的细节:只允许 CDN 官方出口网段访问源站,通常只能证明请求来自该厂商的边缘节点,不能证明它来自你自己的站点配置。如果厂商出口网段是多租户共享的,其他租户的边缘请求也可能落在同一批源地址内。因此,只有 IP 白名单时,不宜把代理头视为已经完成了租户级认证。

建议在网段限制之外再加一层 CDN 到源站的认证:

  1. 优先使用厂商支持的源站 mTLS / Authenticated Origin Pull,让 Nginx 只接受由指定客户端 CA 或证书签发的回源连接。
  2. 若暂不支持 mTLS,可让 CDN 覆盖写入一个仅用于回源认证的高熵请求头,Nginx 精确校验后才进入业务 location;客户端同名头必须在边缘被删除或覆盖。这个值不要写入访问日志、错误页或响应,并建立轮换流程。
  3. Host 白名单可以减少误路由,但它不是认证手段;源站地址泄露后,调用方可以自行构造 Host。
  4. 认证应在应用之前失败。缺少或携带错误认证信息的请求直接返回 403,且不要再解析其 X-Forwarded-For、X-Forwarded-Proto 等头。

验收可以分三条路径:正常经自有 CDN 配置访问应成功;直连源站应被网络层拒绝;从允许的 CDN 地址到达但缺少或携带错误源站凭据的请求应在 Nginx 被拒绝,应用日志中不应出现该请求。日志只记录认证结果和请求 ID,不记录凭据本身。

这样信任链才是“厂商网段 + 自有分发配置的源站凭据”共同成立,再由 Nginx 接受 CDN 生成的真实 IP 和原始协议头。

飒飒Lv1#7

再补一个信任边界上的细节:只允许 CDN 官方出口网段访问源站,通常只能证明请求来自该厂商的边缘节点,不能证明它来自你自己的站点配置。如果厂商出口网段是多租户共享的,其他租户的边缘请求也可能落在同一批源地址内。因此,只有 IP 白名单时,不宜把代理头视为已经完成了租户级认证。

建议在网段限制之外再加一层 CDN 到源站的认证:

  1. 优先使用厂商支持的源站 mTLS / Authenticated Origin Pull,让 Nginx 只接受由指定客户端 CA 或证书签发的回源连接。
  2. 若暂不支持 mTLS,可让 CDN 覆盖写入一个仅用于回源认证的高熵请求头,Nginx 精确校验后才进入业务 location;客户端同名头必须在边缘被删除或覆盖。这个值不要写入访问日志、错误页或响应,并建立轮换流程。
  3. Host 白名单可以减少误路由,但它不是认证手段;源站地址泄露后,调用方可以自行构造 Host。
  4. 认证应在应用之前失败。缺少或携带错误认证信息的请求直接返回 403,且不要再解析其 X-Forwarded-For、X-Forwarded-Proto 等头。

验收可以分三条路径:正常经自有 CDN 配置访问应成功;直连源站应被网络层拒绝;从允许的 CDN 地址到达但缺少或携带错误源站凭据的请求应在 Nginx 被拒绝,应用日志中不应出现该请求。日志只记录认证结果和请求 ID,不记录凭据本身。

这样信任链才是“厂商网段 + 自有分发配置的源站凭据”共同成立,再由 Nginx 接受 CDN 生成的真实 IP 和原始协议头。

这条补充很关键,但落地时还要再确认两件事,否则可能仍只建立了厂商级信任,而不是站点级信任。

第一,部分 CDN 的 Authenticated Origin Pull 使用厂商共享客户端证书。源站只验证厂商 CA 或共享证书时,效果可能仍然只是“请求来自该厂商”。应确认使用的是本账号或本域名独有的客户端证书,并在源站校验证书链及预期的 SAN、指纹或厂商提供的租户标识;证书轮换时也要同时保留明确的旧、新凭据过渡窗口。

第二,使用自定义请求头作为后备认证时,不要默认 real_ip 一定在认证之后执行。ngx_http_realip_module 会在访问控制阶段之前参与处理;若 real_ip_* 配在 http 或 server 级,而认证只在后续位置中完成,被拒绝的请求也可能先改写 $remote_addr,进而污染访问日志或按 IP 限流的判断。

更稳妥的实现顺序是:

  1. 能用站点独有的 mTLS 时,在 TLS 握手阶段拒绝无效回源连接。
  2. 只能用请求头时,让外层虚拟主机只校验直连地址和回源凭据,不启用 real_ip,也不记录凭据值。
  3. 校验通过后,由外层删除客户端可提交的同名头,把 CDN 的规范客户端 IP 和外部协议重建为内部专用头,再转发到仅监听回环地址或 Unix socket 的内层服务。
  4. 内层只信任外层连接,再根据内部专用头恢复地址并转发给应用;认证失败的请求不应到达内层。

验收时除正常、直连和错误凭据三条路径外,再检查错误凭据请求的外层日志:记录的 peer 应仍是 CDN 节点地址,不能变成请求头中伪造的客户端地址;内层及应用日志中应完全没有该请求。这样才能验证认证顺序本身,而不只是看到最终返回了 403。

补一个可以直接作为验收门槛的结论:顺序应是“先认证回源,再解析代理头”。只允许 CDN 官方网段只能证明请求来自该厂商,不能证明来自自己的站点配置;优先用站点独有的 mTLS,后备才用 CDN 覆盖写入的高熵回源头。认证失败应在外层 Nginx 结束,不能转发到内层或应用。若 real_ip 配在 http/server 级,不要假定它会等认证通过后才执行;需要时拆成“外层认证 -> 内层 real_ip -> 应用”,内层只监听回环地址或 Unix socket。

通过认证后,公网只有 HTTPS 就把 X-Forwarded-Proto 固定为 https;双协议则只对白名单里的 CDN 单值做映射,不能透传任意请求头,也不能用回源段的 $scheme 代替。Nginx 已恢复地址且应用只需要最终客户端 IP 时,发给应用的 X-Forwarded-For 可重建为 $remote_addr,不要直接用 $proxy_add_x_forwarded_for 造成重复链;应用侧只信任直接连接它的 Nginx。

验收至少跑正常请求、伪造 X-Forwarded-For/X-Forwarded-Proto、缺失或错误回源凭据,以及 HTTP/HTTPS 两种入口,并同时核对 peer、解析后的客户端 IP、scheme、Host、Location、Cookie 和缓存状态。失败请求的外层日志应仍显示真实连接 peer,内层和应用不应收到它。

顺序可以,先认边界再看头。你问的两点我直接给结论:

  • CDN 到 Nginx 就算走 HTTP,也别用 $scheme 去覆盖外部 X-Forwarded-Proto。$scheme 只代表这一跳回源协议,不代表用户访问协议。要么固定成 https,要么只信 CDN 清洗后的单值并做白名单映射。
  • Nginx 恢复真实 IP 后,发给应用的 X-Forwarded-For 如果只需要最终客户端 IP,最好直接重建成 $remote_addr。别继续 $proxy_add_x_forwarded_for,不然很容易把已恢复出来的 IP 再追加一遍,链会变脏。完整链路如果真要留,放日志里留,不要让应用自己猜。

实际落地时就盯三件事:CDN 是否清洗头、源站是否只信官方回源、应用是否只信 Nginx。nginx -t 过了不代表语义对,最好再拿正常请求、伪造头、直连源站三种情况对一遍日志。