套接字对端还是转发头:该信哪个地址?
套接字对端和转发头是不同的证据。比较两者各自能证明什么,并且只通过你掌控的代理链来信任转发头。
进入你的应用的请求携带两类地址证据:打开网络连接的对端,以及声称更早客户端地址的 HTTP 字段。它们回答不同的问题。套接字对端是被观察到的,而转发头由写入者断言,二者都不应在日志或访问规则中悄然取代对方。下面的比较先把它们分开,再给出头字段真正可用的唯一部署方式。
在比较任何东西之前,先描述路径:
Client -> approved reverse proxy -> application origin在这种部署中,源站的套接字对端通常就是代理,转发头可以携带代理对客户端的断言。它的价值来自与那个代理之间的受控关系,而不是来自头的名字。
两类地址证据
套接字对端是关于一条 TCP 连接的事实:有东西从那个地址完成了握手。它不必然是你关心的那个人、那台设备或那个账号,因为 VPN 或 Tor 出口完全可能合法地成为服务观察到的来源。它仍然是主机自己验证过的唯一地址,其价值也仅在于此。
转发字段是关于一条链路的元数据。X-Forwarded-For、X-Real-IP 以及标准化的 Forwarded 字段可以被任何触及回显它们的端点的客户端写入,因此其可信度完全取决于谁被允许设置它们。
在请求上下文中用不同的名字同时保留两者。用头覆盖对端的应用会失去日后重建信任错误的能力,而那恰恰是你需要它的时候。
头永远不改变连接的走法
把地址写进转发字段不会改变到达服务器的 TCP 连接的来源,也不会改变路径上任何地方的选路。普通 VPN 路由或 源 NAT 完全不依赖任何 HTTP 头来投递流量。同样,过境网关不会在不改变 TLS 端点部署的情况下改写受到妥善保护的 HTTPS 交换。
反向的错误同样常见:HTTPS 并不会让客户端提供的转发头变成真的。它保护一个假断言的方式,与保护真断言完全相同。
如果同事提议改头来修某个路由问题,那么诊断已经错了。去修路径,或者去修写头的代理配置。
指名被允许断言的代理
两条规则让转发字段变得可用,而且两条都属于配置而不是代码。边缘按自己对那些字段的成文行为,规范化或丢弃来自外部的一切。然后源站只从你指名的代理路径取替代地址,别处一律不取。第二条规则只在客户端无法以其他方式抵达源站时成立,因为同时响应直连请求与代理请求的源站,根本不知道自己在服务哪一个。
NGINX 的真实 IP 模块 清楚地展示了这种划分。set_real_ip_from 指名其断言被采信的发送方,real_ip_header 则说明哪个字段承载它;前者负责信任,后者只负责指出该看哪里。
set_real_ip_from 10.20.0.0/24;
real_ip_header X-Forwarded-For;
# real_ip_recursive off - take the rightmost value in the field
# real_ip_recursive on - walk the list back past every trusted address
单靠一条选择头的指令,仍然没有确立任何关于谁发送了该值的东西。
传输安全必须在所信任的组件之间全程生效。当下游某个未受保护的跳点让人在途中改掉断言时,即便规范化正确的边缘也会失去那份价值。
有意识地处理多跳链路
多个代理需要明确的链路模型。不要因为某篇博客那样做,就取逗号分隔的最左或最右值;弄清楚你的代理里哪一个追加、哪一个替换、哪一个保留入站字段。例如 NGINX 可以对照自己的可信集合反向遍历列表,并在遇到第一个不属于自己的地址时停下,那是该产品的规则,而不是每个框架都必须遵守的法律。用同样两个问题去读你自己代理的文档:它选哪个值,它信哪些发送方?
把链路写下来:跳点顺序、哪个跳点设置字段、哪个跳点剥掉入站副本。然后把这份说明放在代理配置旁边,而不是只留在工单里。再通过已批准的验证流程发送一个带伪造头的请求,确认源站忽略它。
增加跳点时,要重读那份文档。摆在可信代理前面的新负载均衡器,可能在某人都没改应用的情况下改变哪个地址胜出。
把两种观察都留在日志里
在允许诊断的地方,把连接对端和规范化后的客户端地址记为两个独立字段,只附加目的真正需要的转发元数据。然后用两个请求验证部署:一个经由代理、应当合理记录两个值;另一个直连源站、必须被拒绝。
记住这一对意味着什么。头加上了你选择信任的组件所提供的上下文;对端仍是你的主机实际看到的唯一地址。两者都不是个人身份的证明,任何把其中之一当作证明的设计,都会在某人身上出错。