一、背景
记一次私有云 K8s 项目交付中,Ingress‑NGINX 采用外部 HAProxy 作为四层入口后,发现业务 Pod(如 Nginx)的访问日志记录的并非客户端真实 IP,而是K8s 内部地址或 HAProxy 的地址。要使最终的 Pod 准确采集到客户端真实 IP。
实测后发现,需要同时完成链路"透传":HAProxy 发送 PROXY v2 保留真实 IP,Ingress‑NGINX 打开 透传配置,并由后端应用读取 X‑Forwarded‑For,ingress-nginx 的Service 侧使用 externalTrafficPolicy=Local 保源 IP。
可以看我👇的图:

二、实战
在实战开始之前,我们需要先了解下 K8s service中的externalTrafficPolicy参数
2.1 关于service中的externalTrafficPolicy参数
两种取值
- Cluster(默认):集群内访问会负载到任意就绪后端,可跨节点。
- Local:仅转发给"源节点的本地后端",没有本地后端则失败;适合就近访问、降低跨节点带宽/延迟,或依赖本地资源。
2.1.1 当 service 的externalTrafficPolicy=Cluster
集群内访问会负载到任意就绪后端,可跨节点。目标是任何节点都能访问
-
我们以一个案例:K8s中运行一个Nginx ,以 NodePort 暴露
#demo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80apiVersion: v1
kind: Service
metadata:
name: nginx
spec:
type: NodePort
selector:
app: nginx
externalTrafficPolicy: Cluster
ports:
- port: 80
targetPort: 80
protocol: TCP
nodePort: 31043
yaml
#apply应用
$ kubectl apply -f demo.yaml
deployment.apps/nginx created
service/nginx created
#查看 pod 状态以及 nodeport 端口
$ kubectl get pod,svc
NAME READY STATUS RESTARTS AGE
pod/nginx-5869d7778c-4z7kd 1/1 Running 0 15s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 7d
service/nginx NodePort 10.111.180.29 <none> 80:31043/TCP 7s


现象与结论:
- master / worker 任意节点的 31043 都能访问。
- 源 IP 通常是节点 IP,不保留真实客户端 IP。

2.1.2 当 service 的externalTrafficPolicy=Local
仅转发给有源节点的本地后端,没有本地后端则失败
-
我们还是以一个案例:K8s中运行一个Nginx-1 ,以 NodePort 暴露
#demo-1.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-1
spec:
replicas: 1
selector:
matchLabels:
app: nginx-1
template:
metadata:
labels:
app: nginx-1
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80apiVersion: v1
kind: Service
metadata:
name: nginx-1
spec:
type: NodePort
selector:
app: nginx-1
externalTrafficPolicy: Local
ports:
- port: 80
targetPort: 80
protocol: TCP
nodePort: 32043#apply应用
$ kubectl apply -f demo-1.yaml
deployment.apps/nginx-1 created
service/nginx-1 created#查看 pod 状态以及 nodeport 端口
$ kubectl get pods -n default nginx-1-74569994c4-65wjx -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-1-74569994c4-65wjx 1/1 Running 0 3m40s 10.244.135.156 k8s-node03 <none> <none>$ kubectl get svc -n default nginx-1 -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
nginx-1 NodePort 10.96.39.34 <none> 80:32043/TCP 3m19s app=nginx-1
看到 nginx-1 这个 pod 运行在 k8s-node3 节点上,我们试下用不同的节点访问,看下对比的效果


现象与结论:
- 只有 k8s-node3 节点的 ip 可以访问到,且保留了真实客户端 IP
- 如需让某节点可达,必须在该节点上跑后端 Pod(role/容忍/DaemonSet)

2.2 Ingress-nginx 的 service 修改为 externalTrafficPolicy=Local
-
部署 ingress-nginx 的类型为 deployement,三副本方式;且只部署在所有的 node 节点上,master 节点不参与外部的流量转发,当然也可以调整成DaemonSet。
-
Ingress-nginx 的 service 类型为 Nodeport
-
修改 externalTrafficPolicy: Local
apiVersion: v1
kind: Service
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.14.0
name: ingress-nginx-controller
namespace: ingress-nginx
spec:
externalTrafficPolicy: Local
ipFamilies:
- IPv4
ipFamilyPolicy: SingleStack
ports:
- appProtocol: http
name: http
port: 80
protocol: TCP
targetPort: http
nodePort: 32280
- appProtocol: https
name: https
port: 443
protocol: TCP
targetPort: https
nodePort: 32443
selector:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
type: NodePort -
查看ingress 命名空间下的所有资源对象
$ kubectl get all -n ingress-nginx
NAME READY STATUS RESTARTS AGE
pod/ingress-nginx-controller-6f58887cd5-d4mxn 1/1 Running 0 3d17h
pod/ingress-nginx-controller-6f58887cd5-t8hhq 1/1 Running 0 3d17h
pod/ingress-nginx-controller-6f58887cd5-xszld 1/1 Running 0 3d17hNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/ingress-nginx-controller NodePort 10.96.227.186 <none> 80:32280/TCP,443:32443/TCP 3d17h
service/ingress-nginx-controller-admission ClusterIP 10.106.60.85 <none> 443/TCP 3d17hNAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/ingress-nginx-controller 3/3 3 3 3d17hNAME DESIRED CURRENT READY AGE
replicaset.apps/ingress-nginx-controller-6f58887cd5 3 3 3 3d17h
2.3 Ingress-NGINX 的 ConfigMap 开启 PROXY Protocol 与 X-Forwarded-For
-
Ingress-nginx 的部署内容省略,可直接修改 configmap.data 中定义的内容
--
apiVersion: v1
data:
use-proxy-protocol: "true"
compute-full-forwarded-for: "true"
forwarded-for-header: "X-Forwarded-For"
kind: ConfigMap
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.14.0
name: ingress-nginx-controller
namespace: ingress-nginx -
参数解读
use-proxy-protocol:启用 PROXY 协议(v1/v2),让 NGINX 从负载均衡器/代理接收客户端的真实 IP、端口等信息(需上游也支持 PROXY 协议)。
compute-full-forwarded-for:当启用时,NGINX 会将客户端的原始 IP 追加到 X-Forwarded-For头中(而非直接覆盖),保留完整代理链。
forwarded-for-header:指定用于存储客户端 IP 的头部名称(默认是 X-Forwarded-For,可自定义为其他如 X-Real-IP等)。
2.4 使用Nginx自带的Realip模块获取用户真实IP
-
Nginx 默认只能看到 上一层代理的 IP ,上面的案例我们之所以可以看到真实的 ip 地址,是因为只经过了一层转发。现在我们的访问是
客户端 → Haproxy(最外侧)→ Ingress-NGINX → Nginx Pod(你的服务) 。每一层代理都会修改请求的元数据(如 IP 头),Nginx 默认只能看到「上一层代理的 IP」(而非真实客户端 IP)。下面的这个三个参数就是为了让你的 Nginx Pod 从多层代理的混乱 IP 中"捞出"真实客户端 IP。
-
这段 Nginx 配置属于
ngx_http_realip_module模块 (用于处理"真实客户端 IP"),核心作用是:当 Nginx 前方有代理(如负载均衡器、CDN、Ingress)时,从代理传递的 HTTP 头中提取客户端的真实 IP,并替换 Nginx 默认的$remote_addr变量 (默认$remote_addr是上一层代理的 IP,而非真实客户端 IP)。#指定「可信代理服务器的 IP 段」,10.244.0.0/16 是k8s 的子网地址,表示信任只信任这段ip的代理头,即任何代理传递的 X-Forwarded-For都被认为是合法的,一般我们会添加上游服务器的 IP 地址。
set_real_ip_from 10.244.0.0/16;#指定「从哪个 HTTP 头中提取真实客户端 IP
real_ip_header X-Forwarded-For;#递归跳过可信代理 IP,取第一个非可信 IP(真实客户端 IP)
real_ip_recursive on; -
创建一个 main-app 的案例 : 包含 configmap,deployment,svc
main-app.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-main-conf
namespace: default
data:
nginx.conf: |
user nginx;
worker_processes auto;error_log /var/log/nginx/error.log notice; pid /run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; set_real_ip_from 10.244.0.0/16; real_ip_header X-Forwarded-For; real_ip_recursive on; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; keepalive_timeout 65; include /etc/nginx/conf.d/*.conf; }
apiVersion: apps/v1
kind: Deployment
metadata:
name: main-app
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: main-app
template:
metadata:
labels:
app: main-app
spec:
containers:
- name: main-app
image: nginx:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
volumeMounts:
- name: nginx-main-conf
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
readOnly: true
volumes:
- name: nginx-main-conf
configMap:
name: nginx-main-confapiVersion: v1
kind: Service
metadata:
name: main-app-service
namespace: default
spec:
type: ClusterIP
selector:
app: main-app
ports:
- name: http
port: 80
targetPort: 80#apply
kubectl apply -f main-app.yaml#查看资源
[root@k8s-master01 ~]# kubectl get all -n default
NAME READY STATUS RESTARTS AGE
pod/main-app-895f5b55d-t8tjq 1/1 Running 0 101sNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 7d18h
service/main-app-service ClusterIP 10.100.5.238 <none> 80/TCP 101sNAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/main-app 1/1 1 1 101sNAME DESIRED CURRENT READY AGE
replicaset.apps/main-app-895f5b55d 1 1 1 101s
2.5 配置 Ingress 域名
-
配置一个 TLS Secret 证书,将证书导入到 k8s 的 default 命名空间下的 secret 中
kubectl create secret tls srebro.cn-tls
--cert=srebro.cn.pem
--key=srebro.cn.key
--namespace=default -
创建一个 ingress 规则指定到一个域名上,如 test.srebro.cn
#ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: baseline-sre-test-ingress
namespace: default
annotations:
nginx.ingress.kubernetes.io/use-regex: "true"
nginx.ingress.kubernetes.io/rewrite-target: /$2
# 可选:强制 HTTPS 重定向
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- test.srebro.cn
secretName: srebro.cn-tls
rules:
- host: test.srebro.cn
http:
paths:
- path: /()(.*)
pathType: ImplementationSpecific
backend:
service:
name: main-app-service
port:
number: 80#创建ingress
$ kubectl apply -f ingress.yaml
ingress.networking.k8s.io/baseline-sre-test-ingress created#查看 ingress
$ kubectl get ingress -n default
NAME CLASS HOSTS ADDRESS PORTS AGE
baseline-sre-test-ingress nginx test.srebro.cn 80, 443 22s
2.6 HAProxy 配置 send-proxy-v2参数
-
send-proxy-v2是 HAProxy 的一个配置指令,用于 向后端服务发送「代理协议 v2 版本」的元数据。
-
HAProxy 监听本地的 443 端口,并代理到 所有node 节点上的 Nodeport 端口上。
frontend kube-https
bind *:443
mode tcp
option tcplog
default_backend kube-httpsbackend kube-https
mode tcp
option tcplog
option tcp-check
balance roundrobin
default-server inter 10s downinter 5s rise 2 fall 2 slowstart 60s maxconn 250 maxqueue 256 weight 100
server kube-node01 172.22.33.110:32443 send-proxy-v2 check
server kube-node02 172.22.33.111:32443 send-proxy-v2 check
server kube-node03 172.22.33.112:32443 send-proxy-v2 check -
为了方便测试本地电脑,配置 test.srebro.cn 域名解析到 这个 haproxy 服务器172.22.33.100上
sudo vim /etc/hosts
172.22.33.100 test.srebro.cn

2.7 访问测试
- 页面

- 日志

三、结语
- 问题本质:多层代理(HAProxy → Ingress‑NGINX → Nginx Pod)会改变源地址,后端默认只能看到"上一层代理"的 IP,导致真实客户端 IP丢失。
- 策略选择:externalTrafficPolicy=Cluster:任意节点可达,易用但一般不保留真实 IP(跨节点转发+SNAT),externalTrafficPolicy=Local:仅本地后端节点可达,保留源 IP;入口节点必须有 Ingress Pod。
- 注意事项:set_real_ip_from 只信任你控制的网段(如 K8s Pod 网段),避免被伪造头欺骗。生产中建议仅在 worker 暴露入口;master 不承载外部流量。