
工作这些年和K8s打交道最频繁的场景就是私有云。机房自建、Kubernetes部署、负载均衡一接各种网络上的幺蛾子就来了。其中最让人头疼的就是“拿不到真实客户端IP”。特别是下面这种链路客户端请求先进HAProxy再转到K8s的Nginx Ingress最后落到Pod里的Nginx。三层代理叠在一起等日志到Nginx手里看到的remote_addr只剩一串内网地址不是Node节点IP就是Ingress Pod IP。真要让客户端IP“透传”到底得在HAProxy、Ingress、后端Nginx三个环节一起配。这篇文章就是一份完整的实战记录基于HAProxy Nginx Ingress这套典型的K8s私有云入口链路把真实客户端IP透传到底层Nginx。我会先讲清楚为什么IP会丢再给出几种方案选型最后把HAProxy、Ingress Controller、后端Nginx的配置和验证方法逐层展开。适合正在被IP失真问题折磨的K8s运维、DevOps也适合刚接手私有云集群想把访问链路理顺的朋友。1. 问题背景两层代理为何吞掉真实IP1.1 私有云K8s的流量链路先看一条很常见的链路。客户端IP是203.0.113.5访问的是某私有云提供的业务域名。流量从公网或办公网进来先到机房边界的HAProxy集群HAProxy前面往往还有Keepalived虚拟IP这里为了简化先不提。HAProxy收到请求后会按后端配置把请求转发给K8s集群里的Ingress Controller常见的暴露方式是NodePort。Ingress Controller处理完路由再把请求交给K8s Service最终由kube-proxy转发到任意一个后端PodPod里跑着Nginx。这条链路听起来并不复杂但每一跳都会改写源IP。HAProxy转发时默认会用自身和后端建立新的TCP连接源IP自然就变成HAProxy自己的内网地址。Ingress Controller转发到后端时如果走的是K8s Service的ClusterIPkube-proxy默认开启SNATPod收到的源IP又变成了Node节点IP。到了最后Nginx这一层它看到的remote_addr可能是192.168.1.21这种NodeIP也可能是10.244.1.23这种Ingress Pod IP反正不是客户端真实的203.0.113.5。这就是“透传”问题的根源。每一层代理都像一个中转站如果不显式处理源IP它只会告诉你“上一站是谁”不会告诉你“最初从哪来”。所以想要真正的客户端IP必须在每一层都做透传配置。这里特别要提醒K8s Service的SNAT不是通过什么特殊开关来控制它由externalTrafficPolicy决定后面实战部分我会重点拆解。1.2 为什么不能只靠一个X-Forwarded-For有人可能会说HAProxy不是会自动加X-Forwarded-For头吗后端Nginx用$http_x_forwarded_for不就能取到客户端IP了这个想法对了一半。HAProxy确实会在HTTP模式下自动向请求追加X-Forwarded-For头里面带上客户端IP。后端Nginx确实也能通过$http_x_forwarded_for这个变量读到这个头。但问题在于读取和信任是两回事。Nginx默认情况下$remote_addr还是上一层的IP任何限流、ACL、日志分析如果用$remote_addr依然拿不到真实IP。而$http_x_forwarded_for是个HTTP头客户端完全可以伪造——直接在请求里带个X-Forwarded-For: 8.8.8.8如果HAProxy没清掉后面所有层都会信以为真。所以正确做法是后端Nginx开启real_ip模块通过set_real_ip_from指定“哪些地址来的流量是可信代理”再告诉它真实IP在哪个头里Nginx才会把$remote_addr替换成真实客户端IP。这还没完因为Ingress Controller层也需要信任上游HAProxy传过来的XFF头不然它可能丢弃或重写这个头或者向前面再伪造一次。这就是为什么配置要贯穿三层任何一层偷懒结果都会变形。我顺便补充一下Ingress Controller的use-forwarded-headers这个开关很多人忽略了它的意义。它控制Nginx Ingress是否信任外部传入的X-Forwarded-For等头。默认情况下Nginx Ingress虽然自己在转发时会加XFF但是并不“完全信任”用户传入的那个XFF头涉及真实IP计算时可能用链路上游节点地址。开启后它就以上游传进来的XFF头为准了。理解了这一点后面配置起来就不会两眼一抹黑。1.3 IP失真后的实际影响真实IP拿不到不只是日志不好看的问题。首当其冲的是基于IP的访问控制。比如你给管理后台做了IP白名单只允许公司出口IP访问结果Nginx看到的全是Ingress Pod的内网地址白名单要么放全网段形同虚设要么干脆把所有人挡在外面。再比如限流Web应用靠IP做频控IP全是假的频率限制基本报废。日志分析也是重灾区。私有云里的Nginx日志如果记录的是内网代理地址统计出来的访问来源毫无意义按IP维度做聚合报表直接没法用。安全审计上更麻烦出了异常流量想从访问日志里溯源到具体客户端结果每一行都是10.244.x.x等于没有日志。地域分流、按运营商调度这些功能同样依赖真实IP没有它全都跑不起来。所以说真实客户端IP透传看着是个网络小问题实际是日志、风控、安全、调度这些上层功能的地基。地基不牢后面做什么都别扭。2. 方案选型HAProxy 透传真实IP的三种思路2.1 HTTP层方案option forwardfor 加 X-Forwarded-For第一种思路最简单也最常用让HAProxy在HTTP模式下主动把客户端IP写进X-Forwarded-For头然后Ingress和后端Nginx逐层信任和处理这个头。HAProxy里的关键配置是“option forwardfor”。这段配置可以写在frontend也可以写在backend但一般情况下写在frontend更稳妥因为前端每来一个请求都从源IP开始算避免后端叠加过头。我习惯的做法是frontend k8s_ingress bind 192.168.1.10:80 mode http option forwardfor http-request set-header X-Forwarded-Proto http if !{ ssl_fc } http-request set-header X-Forwarded-Proto https if { ssl_fc } default_backend k8s_ingress_backend backend k8s_ingress_backend mode http server k8s-node1 192.168.1.21:30080 check server k8s-node2 192.168.1.22:30080 checkoption forwardfor做了两件事一是如果客户端请求没带X-Forwarded-For就生成一个二是如果带了就在后面追加一段。默认追加的格式是“客户端IP, HAProxy的IP”。这样到了后端XFF头部里最后几项就记录了每一层代理。为了让Ingress能正确拿到第一个客户端IP最好在HAProxy上确保XFF头不会被人伪造。训练前文也提过我一般会加清洗规则后文专门讲。这个方案的优点是协议层面纯HTTP后端应用即使不做real_ip配置也至少能在$http_x_forwarded_for里读到真实IP缺点是每个处理环节都要正确配置信任策略否则容易被绕过或误读。2.2 TCP层方案Proxy Protocol透传源IP第二种思路是在四层TCP上直接带着源IP包走叫Proxy Protocol。HAProxy不需要解析HTTP头直接在TCP连接建立后发送一段额外协议头把客户端IP、端口写进去。Ingress Controller收到后解析这段协议头恢复出真实源IP。HAProxy四层配置大概是frontend k8s_ingress bind 192.168.1.10:80 mode tcp default_backend k8s_ingress_backend backend k8s_ingress_backend mode tcp server k8s-node1 192.168.1.21:30080 send-proxy-v2 check server k8s-node2 192.168.1.22:30080 send-proxy-v2 check注意这里是“mode tcp”不能再用option forwardfor。同时后端必须支持Proxy Protocol否则解析会出错。Nginx Ingress是支持这个能力的需要在ConfigMap里把proxy-protocol设为true然后它才能接受这种带自定义协议头的连接。Proxy Protocol的特点是源IP信息不依赖HTTP头比XFF更“硬”不容易伪造。但也有代价如果链路中间还有不支持的负载均衡配置会非常麻烦而且HTTP层和TCP层混在一起时HTTPS证书卸载的位置也会受影响。我自己的经验是如果前面只有HAProxy一层入口且Ingress Controller也用默认ConfigMap用Proxy Protocol完全可行但如果链路很长或者有第三方设备硬插监控HTTP层方案反而更好排查。2.3 我的选型倾向HTTP转发 use-forwarded-headers实际项目里我用得最多的组合是HAProxy模式HTTP option forwardfor然后Nginx Ingress开启use-forwarded-headers最后后端Nginx再用real_ip模块收尾。原因有几个。首先是排查方便。HTTP头在任何抓包工具里都能看到出了问题直接用curl加X-Forwarded-For头模拟链路走一遍就清楚了。Proxy Protocol的协议头得用tcpdump抓原始字节才好看对不熟悉的人不太友好。其次是兼容性。后端Nginx只要配置real_ip模块就能把$remote_addr替换成真实IP正在运行的业务不需要改端口、不需要改协议应用层几乎无感知。而Proxy Protocol需要Ingress在监听端口开启对应协议一旦开错健康检查都会失败。还有一点很重要私有云环境里HAProxy和K8s集群通常在一个可信内网只要HAProxy认真做好XFF头清洗XFF方案的可靠性并不比Proxy Protocol差。当然如果业务对“真实IP”的严谨性要求极高或者直接面对公网、攻击面比较大我会建议上Proxy Protocol把伪造的可能性从协议上掐死。没有绝对完美的方案只有适合当前链路情况的方案。2.4 方案对比速查表维度HTTP方案X-Forwarded-ForTCP方案Proxy Protocol传输层级7层HTTP4层TCP配置复杂度低高两端都要支持防伪造能力依赖HAProxy清洗天然防伪造排查难度低curl可直接模拟高需要抓包适用场景私云入口、七层路由公网入口、安全要求高、直连Ingress这表看过就有数了。如果你的Ingress前面就是HAProxy一个入口家里又全是内网环境XFF方案已经够用。如果你要做到跟公有云SLB一样“无论谁在中间都要把源IP传下去”那Proxy Protocol更适合。3. 完整实战从HAProxy到Ingress到后端Nginx的透传配置3.1 环境说明先交代一下环境方便你对照两台HAProxyKeepalived做VIPVIP地址192.168.1.10版本2.4K8s集群版本1.28Ingress使用Nginx Ingress Controller部署在ingress-nginx命名空间Service暴露方式为NodePortK8s节点IP192.168.1.21、192.168.1.22后端业务Pod内跑的是官方Nginx镜像用来做日志验证。目标很明确不管请求从哪来最终Pod里的Nginx日志记录到的remote_addr必须是真实客户端IP而进入HAProxy之前的所有内网转发细节都不能污染这个值。这里我要特意说明一下很多朋友以为只要配置Ingress的use-forwarded-headers就够了但其实后端Nginx那步才是最后一道坎。只要后端Nginx没做real_ip配置$remote_addr永远是Ingress Controller的Pod IP或Node IPuse-forwarded-headers只是让Ingress正确传递XFF头并不会替你改掉$remote_addr。3.2 HAProxy侧配置我在前面例子基础上加上了XFF清洗生产环境里建议这样做。重点是在frontend里显式控制X-Forwarded-For而不是让option forwardfor自由追加。因为如果客户端恶意带一个“X-Forwarded-For: 1.1.1.1”进来option forwardfor会在后面追加真实IP但某些后端程序可能只取第一个IP就会把1.1.1.1当成用户IP。清洗逻辑是先删掉客户端传来的XFF头再重新用真实源IP赋值。frontend k8s_ingress bind 192.168.1.10:80 mode http http-request del-header X-Forwarded-For http-request set-header X-Forwarded-For %[src] http-request set-header X-Forwarded-Proto http if !{ ssl_fc } http-request set-header X-Forwarded-Proto https if { ssl_fc } default_backend k8s_ingress_backend backend k8s_ingress_backend mode http server k8s-node1 192.168.1.21:30080 check server k8s-node2 192.168.1.22:30080 check注意我用了“del-header”再加“set-header”而不是直接set-header。直接set-header其实也能覆盖但del一遍再set能在语义上更明确这条链路里不允许客户端自带XFF。另外如果有SSL建议同时把X-Forwarded-Proto设置正确否则后端Nginx在做http跳https或者判断协议时会出问题。配置完用下面命令检查语法haproxy -c -f /etc/haproxy/haproxy.cfg没问题就reload。reload不会断现有连接这在生产环境里是个优势。3.3 K8s Ingress Service 调整externalTrafficPolicyLocal这一步很多人会漏。如果Ingress Controller使用的是NodePort Service并且默认的externalTrafficPolicy是Cluster模式那么流量进入NodePort后kube-proxy做转发时会SNAT源IP变成节点IP。在这种模式下即使HAProxy把客户端IP写进了XFF头Ingress Controller收到的TCP源IP已经不是HAProxy的了虽然XFF头还在但Ingress内部的真实IP计算可能因为use-forwarded-headers配置而“信任”外部头所以有些场景还能用。但如果你想拿到更严谨的链路尤其是以后想升级为Proxy Protocol必须把externalTrafficPolicy改成Local。执行kubectl patch svc ingress-nginx-controller -n ingress-nginx \ -p {spec:{externalTrafficPolicy:Local}}改完后K8s只会在流量落到“有Ingress Controller Pod”的节点上时做转发并且不再SNAT源IP保持为HAProxy的内网地址。此时Ingress Controller看到的remote_addr就是192.168.1.10HAProxy内网地址这比看到某个随机NodeIP要干净得多。同时要注意Local模式下健康检查会走healthCheckNodePortNodePort端口并不会在没Pod的节点上正常响应所以HAProxy的check最好指向健康检查端口。查询健康检查端口kubectl get svc ingress-nginx-controller -n ingress-nginx -o jsonpath{.spec.healthCheckNodePort}假设返回31234那HAProxy里的check可以写成server k8s-node1 192.168.1.21:31234 check server k8s-node2 192.168.1.22:31234 check如果所有节点上都跑着Ingress副本用原来的NodePort也没问题但按最佳实践来配置会稳一点。另外Local模式下如果某个节点没有Ingress Pod那么这个节点收到流量后会被直接丢弃或转发到其他节点时再次SNAT所以建议给Ingress Controller分配一个比较集中的NodeSelector保证每个可调度节点上都有副本。3.4 Ingress Controller 开启 use-forwarded-headers现在让Ingress Controller信任来自HAProxy的XFF头。Nginx Ingress的很多行为都由ConfigMap控制。进入ingress-nginx命名空间编辑ConfigMapkubectl edit configmap ingress-nginx-controller -n ingress-nginx在data段增加use-forwarded-headers: true保存后Ingress Controller会自动reload。这里要特别说一下use-forwarded-headers一旦开启Ingress Controller会优先使用外部传入的X-Forwarded-For、X-Forwarded-Proto等头来构建真实IP和协议判断。这在大规模私有云中是可行的但如果你的Ingress前端直接暴露在公网并且没有清洗XFF头那伪造IP的漏洞就很大。公有云场景里这个开关通常会配合信任的负载均衡网段来使用。Nginx Ingress还提供了forwarded-for-header、compute-forwarded-headers这些选项可以细调但私云里做好HAProxy清洗就够了。此外如果你的K8s里有多个Ingress Controller副本ConfigMap修改后它们都会加载。观察滚动日志确认没有报错kubectl logs -n ingress-nginx -l app.kubernetes.io/nameingress-nginx --tail100 | grep -i error如果日志干净再继续下一步。3.5 后端Nginx配置 real_ip最后一步后端Pod里的Nginx配置。官方Nginx镜像的默认配置里已经有log_format main但默认的remote_addr只会记录Ingress Controller的地址。我们需要在nginx.conf的http块里增加下面这段set_real_ip_from 192.168.1.10; # HAProxy内网IP set_real_ip_from 192.168.1.21; # Node节点IP set_real_ip_from 192.168.1.22; set_real_ip_from 10.244.0.0/16; # Pod网段如果Ingress以Pod间直连方式访问后端 real_ip_header X-Forwarded-For; real_ip_recursive on;set_real_ip_from表示“这些来源的连接是可信代理它们传来的XFF头可以拿去计算真实IP”。要写全所有可能成为上游的网段。real_ip_header指定真实IP信息在哪个头里。real_ip_recursive on很关键它告诉NginxXFF头里可能有多级代理地址要递归计算出最左边的那个客户端IP。如果不开recursiveNginx可能只会把XFF头最后一个内网代理IP当作真实IP结果还是错的。配置完reload Nginx。如果你用的是Docker里的Nginx可以用docker exec container nginx -s reload如果是K8s Pod需要先更新ConfigMap或直接修改配置文件后重启Pod。为了快速验证我一般用一个单独的测试Pod挂载准备好的nginx.conf反复调。3.6 验证真实客户端IP是否透传成功验证分两步。第一步是模拟请求第二步是在后端Nginx日志里看IP。模拟请求从一台外部测试机发起curl -s -o /dev/null -w %{http_code}\n http://demo.example.com/test然后进入后端Pod查看日志kubectl exec -it backend-pod -- tail -f /var/log/nginx/access.log如果配置成功日志里第一项应该是真实客户端IP比如203.0.113.5 - - [12/Sep/2025:10:21:37 0800] GET /test HTTP/1.1 200 5 - curl/8.1.2如果还看到192.168.1.10、192.168.1.21说明中间某层没生效。我建议顺着链路逐步看先看Ingress Controller的日志里的$remote_addr再看后端Pod的日志确定在哪一层断的。Ingress Controller日志可以用kubectl logs -n ingress-nginx ingress-pod -f --tail200观察其中的client地址。这里给一个对比经验没做任何配置时后端Nginx日志显示的是10.244.1.23Ingress Pod IPIngress日志显示的是192.168.1.10或NodeIP做了完整配置后后端应该是203.0.113.5Ingress日志里的client也应该是203.0.113.5。完全对上才算链路打通。4. 踩坑实录真实客户端IP透传中的典型问题与排查4.1 externalTrafficPolicy 忘了改源IP又变成NodeIP这是个非常典型的坑。Ingress Service默认为Cluster模式流量转发到Pod时会SNATPod看到的是节点IP。你明明配好了HAProxy的option forwardforIngress也开了use-forwarded-headers后端也配了real_ip但日志里real_ip还是一串NodeIP。为什么因为real_ip_header依赖XFF如果Ingress Controller传给后端时把XFF头丢了或改了或者它自己计算真实IP时用错了值就会这样。遇到这种情况第一步就是检查Ingress Service的externalTrafficPolicykubectl get svc -n ingress-nginx ingress-nginx-controller -o jsonpath{.spec.externalTrafficPolicy}如果不是Local先改成Local。改完会在外部流量进入时保留源IPIngress Controller就能看到HAProxy的内网IP。再配合use-forwarded-headersXFF头就能被信任和正确传递。很多教程只提到改ConfigMap不提这个Policy但实测下来不改它大概率会失败或者结果时好时坏非常烦人。4.2 同时开了 use-forwarded-headers 和 proxy-protocol导致解析错乱我有一阵为了追求极致想把HAProxy的TCP层Proxy Protocol和Ingress的use-forwarded-headers一起用。结果Ingress Controller直接报错后端解析出来的IP完全不对。原因很简单use-forwarded-headers是处理HTTP层的XFF头而proxy-protocol是处理TCP层的连接头两者属于不同透传机制同时开启会让Nginx Ingress内部对真实IP的来源产生歧义。正确选择是二选一。如果HAProxy那边用option forwardfor那Ingress就开use-forwarded-headers不要开proxy-protocol。如果HAProxy那边用send-proxy-v2那Ingress ConfigMap里就开proxy-protocol不需要再开use-forwarded-headers。我在生产上见过有人在HAProxy 7层模式里加send-proxy然后Ingress也开proxy-protocol结果后端口看到的IP依旧是错误值。那不是配置不够是模式都掺在一起了。时刻记住HTTP头方案和Proxy Protocol方案要分开走。4.3 伪造的X-Forwarded-For顺手就能骗过限流我前文提到HAProxy要清洗XFF头实践里真有人试图用“X-Forwarded-For: 1.1.1.1”去干坏事。如果不清洗Ingress和后端都会信以为真后面做基于IP的限流就是形同虚设。攻击者每秒构造不同XFF头的请求就能绕过IP频率限制更何况日志分析也会被污染。所以ha.cfg里那两行真的很重要http-request del-header X-Forwarded-For http-request set-header X-Forwarded-For %[src]不管客户端带什么头HAProxy都会用真实源IP覆盖。如果你不想一刀切也可以加个ACL只清洗来自公网面的请求内网信任面可以跳过。但私有云入口一般就是HAProxy一层直接全局清洗最省心。4.4 real_ip_recursive on 忘开日志里多出一个内网IP有次配置完成后日志里第一项是192.168.1.10后面XFF头里明明有203.0.113.5。排查半天发现就是real_ip_recursive没有开启。Nginx的real_ip模块不是默认递归解析XFF头的不开recursive时它会取XFF头的最后一个地址作为真实IP。在多级代理链路中最后一个地址往往是最近一跳的内网代理地址比如HAProxy自己的IP而不是最初的客户端IP。把real_ip_recursive on加上后Nginx会从左往右找第一个“不在set_real_ip_from信任范围”的地址那通常就是真实客户端IP。这个开关很小但写上就没那么多幺蛾子。我建议所有有多级代理的场景都把它加上别纠结什么“默认行为可能更安全”在私有云内网里recursive on就是最实用的选择。4.5 常见问题速查表症状可能原因检查方法解决办法后端日志显示NodeIPexternalTrafficPolicy非Local查看Service字段改为Local后端日志显示HAProxy内网IP后端real_ip_recursive未开启检查nginx配置打开real_ip_recursive onIngress日志不显示客户端IPuse-forwarded-headers未开启查看ConfigMap设为trueXFF头里出现伪造IPHAProxy未清洗XFF抓包或查看请求头del-header set-header健康检查失败Local模式下检查端口配置错误查看healthCheckNodePortcheck指向健康检查端口这张表基本覆盖了我遇到过的八成问题照着从上往下排查能省不少时间。5. 一些血泪经验总结5.1 链路画清楚之前别动手写了这么多最后分享几条我个人在实际操作中的经验。第一先在纸上画清链路标清楚每一层是HTTP还是TCP、谁负责SNAT、谁添加XFF再动手改配置。我踩过的坑有八成都是链路没画明白就上去敲命令导致的。别嫌画图麻烦一行错误的配置在多层转发里排查起来比画十张图还费劲。5.2 验证Pod是调试透传的神器第二验证的时候不要只盯着一处。我会在后端Nginx、Ingress Controller、HAProxy三层各写一个访问日志三个IP对不上就知道是哪一层断了。第三如果后端应用不关心$remote_addr只想在日志里看到真实IP那么做好HAProxy的XFF清洗 应用读取XFF头就够了但如果做限流、ACL、防刷必须让后端Nginx的real_ip模块把$remote_addr改过来。最后再分享一个小技巧在K8s里调试这个链路可以临时跑一个Nginx测试Pod特意只输出remote_addr和XFF头不要挂任何业务。前端发一个带特殊标记的请求一层一层看它怎么变。等链路跑通了再把这个Pod删掉换上真实业务。这种“验证Pod”的做法比反复改业务Pod配置要快得多也安全得多。真实客户端IP透传不算什么高大上的功能但没做好日志分析、安全防护都会出问题。这篇文章里的配置我已经在多个私有云项目里复现过按照HAProxy清洗XFF、Ingress开use-forwarded-headers、后端Nginx开real_ip三步走基本都能打通。如果环境里有特殊网络策略再根据实际情况微调即可。