1. 私有云K8s里的“IP谜局”为什么Nginx看到的总是内网地址先描述一个我常遇到的场景私有云K8s集群部署好之后前面架了一台HAProxy做统一公网入口业务跑在Nginx Ingress和一堆后端Pod里。上线前功能全通上线后业务方跑来提了个很微妙的问题——访问日志里看到的客户端IP全是内网地址要么是某个Node节点IP要么是Pod网段的地址审计没法做限流也全乱了。这个问题为什么会存在得从流量路径说起。通常一个私有云K8s集群的访问链路是客户端 → 边界HAProxy → K8s任意Node的NodePort → 调度到某个Ingress Controller Pod → 再转发给业务Pod。链路里有两个“断点”。第一个断点在HAProxy到NodePort这一段。如果HAProxy用的是mode tcp四层转发它只负责把TCP流原封不动地搬到后端端口既不解析HTTP头也不往里面塞任何客户端信息。TCP流到了NodePortKube-proxy还要做一次DNAT/SNAT把数据包转发给Ingress Controller Pod。于是Ingress收到的TCP连接源地址已经是Node节点的内网IP了。更麻烦的是如果集群用了多网卡、多个Node做NodePort你甚至可能看到源地址是某一台机器上的VIP或网桥地址。第二个断点在Ingress Controller到业务Pod这一段。Nginx Ingress本身是个反向代理它接收到请求后会用一个新的TCP连接把请求转发给后端业务Pod。这个新连接的源地址是Ingress Pod自己的IP。业务应用如果不做任何处理直接读Nginx变量里的$remote_addr拿到的就是Ingress Pod网段里的某个地址和真实客户端毫无关系。有不少人第一反应是“那我去读HTTP头里的X-Forwarded-For不就行了”。这个想法方向对但实操里坑很多。X-Forwarded-For是标准HTTP头可它有两个致命问题第一它能不能出现、值是什么完全取决于链路上的代理有没有主动写如果整个链路都是四层转发这个头可能压根不存在。第二它是可以被客户端伪造的。随便一个懂HTTP的人都能在请求里预先塞一个假的X-Forwarded-For: 1.2.3.4如果你没有任何信任链校验就信了这个值等于把访问控制和安全审计直接暴露给攻击者。所以解决这件事的完整思路不是“在应用里读某个头”而是从链路入口开始用协议机制把真实客户端IP一级一级传递下来每一级都只信任自己上游那个代理最终形成一条完整的信任链。有一个专门干这事的协议叫PROXY Protocol最早是负载均衡软件社区搞出来的现在HAProxy和Nginx都原生支持。它的原理是在TCP连接建立之后、正式业务数据发送之前先插一段特殊格式的头部里面写明这条连接的真实源IP、源端口、目标IP、目标端口。四层转发因此也能携带原始客户端地址了。整个链路再加上IP透传之后大致长这样客户端浏览器 │ ▼ 公司边界HAProxy监听80/443 │ L4 TCP流 PROXY Protocol帧 ▼ K8s任意Node的NodePort如30080/30443 │ Kube-proxy调度转发 ▼ Nginx Ingress Controller Pod解析PROXY帧重写remote_addr │ HTTP请求 X-Forwarded-For ▼ 业务应用Pod读取remote_addr或XFF下面每一层要做什么、怎么配、怎么验证我按实战顺序拆开讲。2. 先从HAProxy动手四层透传和七层改写怎么选2.1 方案A四层TCP PROXY Protocol推荐的大头方案大部分私有云场景我建议把HAProxy作为纯粹的TCP负载均衡器来用业务侧的七层能力全交给Nginx Ingress。这样HAProxy不解析HTTP、不终止TLS、不做任何应用层判断转发性能最高也最稳定。在HAProxy上的配置大概是这样的。假设后端两个Node节点NodePort选了30080和30443frontend fe_k8s_http bind *:80 mode tcp option tcplog default_backend be_k8s_ingress_http backend be_k8s_ingress_http mode tcp balance roundrobin server k8s-node1 192.168.1.11:30080 send-proxy-v2 check inter 3s rise 2 fall 3 server k8s-node2 192.168.1.12:30080 send-proxy-v2 check inter 3s rise 2 fall 3frontend fe_k8s_https bind *:443 mode tcp option tcplog default_backend be_k8s_ingress_https backend be_k8s_ingress_https mode tcp balance roundrobin server k8s-node1 192.168.1.11:30443 send-proxy-v2 check inter 3s rise 2 fall 3 server k8s-node2 192.168.1.12:30443 send-proxy-v2 check inter 3s rise 2 fall 3关键就在send-proxy-v2这个参数上。它表示HAProxy向本后端转发TCP流时要在业务数据前插入一个PROXY Protocol v2二进制帧里面带这条连接的真实源IP和端口。2.2 send-proxy和send-proxy-v2到底差在哪这里插一个容易踩坑的细节。HAProxy里有两个非常像的参数send-proxy和send-proxy-v2。前者发送PROXY v1文本帧后者发送PROXY v2二进制帧。v1长什么样是一条ASCII文本开头是PROXY TCP4 源IP 目标IP 源端口 目标端口\r\n这样的可读格式。v2则是二进制专门设计成机器可读开头有固定的魔数标记解析效率更高也能携带更多TLV扩展信息。Nginx 1.9.2以后两者都支持所以大多数情况下用哪个都不会出错。我的习惯是统一用v2。原因很实际私有云里这套链路已经够多了每次抓包排查都靠肉眼去看文本帧太累v2结构规范、解析明确两侧版本朝一个方向对齐以后升级维护少一事。2.3 方案B七层HTTP X-Forwarded-For的适用场景也有一些情况需要让HAProxy直接做七层。比如边界就要按域名分流、要做简单的ACL放行或者集群里不用Nginx Ingress而是一组独立Nginx实例直接对外。这种时候四层的PROXY Protocol链路就没法直接用因为HAProxy自己已经消费了HTTP流你需要在转发时主动注入客户端信息。HAProxy七层配置可以这样frontend fe_k8s_http bind *:80 mode http option httplog http-request set-header X-Real-IP %[src] http-request set-header X-Forwarded-For %[src] default_backend be_k8s_ingress_http backend be_k8s_ingress_http mode http server k8s-node1 192.168.1.11:30080 check inter 3s rise 2 fall 3 server k8s-node2 192.168.1.12:30080 check inter 3s rise 2 fall 3这里用%[src]直接把HAProxy看到的客户端源IP写进X-Real-IP和X-Forwarded-For。但注意X-Forwarded-For的值会被Nginx后面用$proxy_add_x_forwarded_for继续追加。如果你不加处理TLS终止时、反复代理时这个头里的地址会越来越长、越叠越乱解析时就得多做一层清洗。还有另一个细节如果你在HAProxy七层改写里只是简单set-header会直接覆盖客户端原本可能带的XFF头。这有时是好事防伪造但如果你还需要原始XFF做链路分析就得换写法。我不能给出一个普适结论只能说生产环境一般按“只相信边界代理注入的那一个值”来设计最稳妥。2.4 两种方案怎么选给个参考表维度四层TCP PROXY Protocol七层HTTP XFF改写HAProxy转发性能高不解析HTTP低一些每个连接都要解析应用层TLS终止位置在Nginx Ingress可放在HAProxy也可放Ingress真实IP准确性很高帧内明确携带源IP高但依赖链路一致且信任边界清晰配置复杂度中HAProxy和Ingress都要开中低但XFF清洗麻烦典型适用场景私有云K8s Nginx Ingress大部分生产边界已有L7需求、非Ingress的Nginx集群我自己的经验是一旦决定未来由Nginx Ingress统一负责七层逻辑就直奔方案A。一次把HAProxy改成mode tcp send-proxy-v2后面所有服务都省心。3. Nginx Ingress层配置listen proxy_protocol与信任边界3.1 让Ingress Controller接收PROXY帧HAProxy改完只是发出去接收侧不认账等于白干。Nginx Ingress Controller默认监听80端口时不会解析PROXY帧你不改它会把这帧当作普通HTTP请求来解析直接报400或者挂断连接。这就是为什么不少人改完HAProxy后马上看到端口全线Down。需要改的是Ingress Controller的启动ConfigMap。如果通过Helm安装最方便的是在values里写入controller: config: proxy-protocol: true set-real-ip-from: 192.168.0.0/16,10.0.0.0/8,172.16.0.0/12如果你已经用kubectl裸部署就编辑对应的ConfigMap名字一般是ingress-nginx-controller位于ingress-nginx命名空间kubectl -n ingress-nginx edit configmap ingress-nginx-controller在那里的data段加上data: proxy-protocol: true set-real-ip-from: 192.168.0.0/16,10.0.0.0/8,172.16.0.0/12数据里的proxy-protocol: true会让Nginx在listen指令里加上proxy_protocol参数。set-real-ip-from则声明只有当连接源地址处于这些内网网段时才解析PROXY帧并把其中携带的客户端IP视为真实IP。这两个参数必须在一起否则你等于无条件信任所有连接上来的客户端都能随便自报IP。改完之后验证一下是否真的生效。执行kubectl -n ingress-nginx exec deploy/ingress-nginx-controller -- cat /etc/nginx/nginx.conf | grep -A 10 listen 80如果看到类似server { listen 80 proxy_protocol;那就说明listen层已经挂上了。3.2 信任链的坑use-forwarded-headers到底该不该开这里必须展开讲一个很多人搞混的配置项use-forwarded-headers。Nginx Ingress Controller的ConfigMap里经常被建议加use-forwarded-headers: true它的作用是让Nginx把请求头里的X-Forwarded-For当作客户端真实IP来源并更新$remote_addr。在“前端是一台七层云负载均衡且那台设备已经重写了XFF”的场景下这是合理的。但在HAProxy四层PROXY Protocol这套链路里我非常不建议随便开这个开关。原因很直接如果开启Nginx会优先根据传入请求里的XFF来重置remote_addr而XFF是可以由客户端自己预埋伪造值的。虽然PROXY帧里带了真实IP但XFF优先参与解析之后真实IP反而容易被假值盖掉。你辛辛苦苦把信任链建好最后却主动把最不可信的一个头当成依据等于安全防线白做。所以我的生产推荐组合是HAProxy侧四层TCP send-proxy-v2Ingress侧proxy-protocol: trueset-real-ip-from: 内网网段use-forwarded-headers不开启或仅在你能确认边界HAProxy已经用七层方式重写XFF时才开启这套组合下真实来源信息由PROXY帧承担Nginx的$remote_addr会被重置为客户端公网IP。而在继续向后端转发时Nginx默认会为上游连接生成或追加X-Forwarded-For头里面带上这个真实IP所以业务Pod依然有标准头可读。3.3 业务Pod侧应该读什么到了业务应用这一层很多人又会纠结到底读$remote_addr还是读X-Forwarded-For我一般建议业务统一读标准头X-Forwarded-For并且按“从右往左取第一个非可信内网地址”的规则解析。因为Nginx Ingress在向业务转发时会在已有的XFF基础上追加它看到的remote_addr而这个remote_addr经PROXY帧重置后就是客户端真实IP。规整的链路里XFF最后一段通常就是客户端。直接读$remote_addr在以后链路变动、插一层代理时又会拿到内网地址编码层面要尽量避免这种“源地址硬绑定”。4. 端到端验证让IP从HAProxy一路亮到业务日志配置改完别急着上线。我建议先做一轮端到端验证确认每一级拿到的IP确实对。验证方法很多我按成本从低到高说。4.1 第一种从业务Pod日志验证先找一个后端业务比如典型的Nginx应用确认它的访问日志格式里有$remote_addr。然后从公网侧发起一个请求比如在外部机器上执行curl -k https://svc.example.com/ --resolve svc.example.com:443:你的HAProxy公网VIP发起后立刻去看业务Pod日志kubectl logs -n 命名空间 deploy/业务Deployment --tail20如果链路配置正确你会看到类似192.0.2.100 - - [17/Feb/2025:10:20:33 0800] GET / HTTP/1.1 200 612 - curl/8.0这里的192.0.2.100就是你测试客户端的公网IP。在透传之前这个位置大概率是一段10.244.x.x或192.168.x.x的内网地址。看到公网IP出现说明链路已经通了。4.2 第二种查看Nginx Ingress Controller日志Ingress Controller自己的访问日志也可以验证。执行kubectl -n ingress-nginx logs -l app.kubernetes.io/nameingress-nginx --tail50里面同样会把remote_addr打进日志。注意Ingress Controller的日志默认包含一堆健康检查、404探测建议用请求路径过滤一下或者加一个带唯一参数的路径来定位。4.3 第三种抓包确认PROXY帧如果前面两种发现IP没透传成功抓包是最快的排障方式。在Ingress Controller Pod里抓NodePort对应的端口比如kubectl -n ingress-nginx exec deploy/ingress-nginx-controller -- tcpdump -ni eth0 tcp port 80 -c 5如果HAProxy正常发出PROXY帧你会看到TCP流里第一条数据不是GET / HTTP/1.1而是PROXY协议特有的帧头。v1文本帧会以PROXY开头v2二进制帧开头有\x0D\x0A加QUIT之类的魔数。看到这些说明HAProxy确实发出、Ingress也确实收到了。如果只看到GET / HTTP/1.1而前面没有PROXY帧基本可以断定HAProxy那侧配置没生效或者后端端口指错了。4.4 用回显服务做一次干净验证我习惯在集群里临时起一个极简回显服务直接返回HTTP头里传入的源信息这样能快速排除业务代码干扰。思路是部署一个小应用在响应里输出Node环境变量和请求头。也可以用现成的httpbin镜像起一个Deployment、Service、Ingress然后从公网访问并观察返回体里的头信息。步骤是部署回显服务 → 从公网curl访问 → 在回显结果里检查X-Forwarded-For的最后一个值。如果它不是内网地址而是你的公网IP整个链路已经验证通过。这种一次性的验证服务很值得维护成标准做法以后每次改负载均衡配置都能先拿它探路。5. 线上踩过的坑与生产配置建议5.1 只改一头另一头还在等普通HTTP最常见的事故是HAProxy侧已经加上了send-proxy-v2但Ingress Controller的ConfigMap没改或者改完没有生效。结果就是Nginx本来在等一个HTTP请求头结果先收到了一段PROXY开头的帧直接判定为非法请求连HTTP协议解析都过不去日志里时不时出现端口Down、502/400。排查方法很简单第一件事永远是看Ingress Controller的listen配置里有没有proxy_protocol。有就往HAProxy方向确认没有就先改Ingress侧。5.2 健康检查通道被PROXY帧冲垮HAProxy后端默认会做健康检查普通的TCP check是建立连接就断开不会发PROXY帧。但Nginx Ingress既然开启了proxy_protocol它会在每次TCP连接建立后等待PROXY帧如果等不到就判定异常健康检查自然全军覆没。表现是HAProxy面板里后端全红但用浏览器访问有时又能通。我的做法是健康检查不直接探测NodePort的80/443而是探测Nginx Ingress Controller自带的管理端口10254。在HAProxy里给后端单独定义一条健康检查比如server k8s-node1 192.168.1.11:30080 send-proxy-v2 check port 10254 inter 3s rise 2 fall 3这样检查端口与业务转发端口分离业务流走80换80健康探针走10254两不干扰。5.3 多层代理时信任链被折叠有些环境里HAProxy前面还有一台云平台自带的负载均衡或者是机房硬件负载均衡。这种多层代理下如果你只在HAProxy上配了PROXY协议上层设备不认识根本没把客户端IP传下来你在HAProxy里看到的源地址就是上层设备的内网地址后面的一切都建立在错误源头上。要先确认最外层设备支不支持转发PROXY协议。支持就逐层配置每层都要声明信任和启用协议不支持就只能退到七层XFF方案在HAProxy里强制set-header注入XFF同时保证最外层设备也覆盖XFF。最怕的是每层都在转发但谁都没真正把真实客户端IP写进去最后一层只是在给自己编故事。5.4 禁止公网直接访问NodePort这套链路成立的前提是“只有你信任的HAProxy能连到NodePort”。如果NodePort直接暴露在公网上任何绕过HAProxy的请求都能直连Ingress而且你还开启了proxy-protocol那一个聪明的攻击者完全可以伪造PROXY帧来伪装任意IP。所以云主机安全组、机房防火墙里一定要限制NodePort只允许HAProxy所在网段访问绝不暴露公网。这是整条链路的底线。5.5 配置变更顺序与回滚预案改这种跨节点的链路配置最忌讳一下子全量上。我踩过一次最疼的坑是在业务高峰期同时刷新HAProxy和Ingress配置结果流量涌入的瞬间后端大量5xx。后来形成习惯先改Ingress侧让它可以接收PROXY帧再用HAProxy的disable server逐个摘掉后端验证一轮没问题再加回去。每一步控制在分钟级出问题随时回滚。说到底IP透传这件事并不难难的是链路里每一层都对“自己是第几个可信代理、下一层该相信什么”有共识。PROXY Protocol解决的是传输层携带源IP的问题XFF解决的是应用层携带源IP的问题两者配合起来再从HAProxy到Ingress到业务Pod逐级验证一次后面就不用再为“为什么日志全是内网IP”头疼了。