MENU

K8s私有云:Nginx真实客户端IP透传全攻略(HAProxy + Ingress 实战)

• January 23, 2026 • Read: 91 • 编码👨🏻‍💻

一、背景

记一次私有云 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: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  type: NodePort
  selector:
    app: nginx
  externalTrafficPolicy: Cluster
  ports:
    - port: 80
      targetPort: 80
      protocol: TCP
      nodePort: 31043
#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

访问:http://K8s中任意节点ip:31043

image-20260123140732295

现象与结论:

  • 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: 80
---
apiVersion: 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          3d17h

NAME                                         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                      3d17h

NAME                                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/ingress-nginx-controller   3/3     3            3           3d17h

NAME                                                  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-conf
---
apiVersion: 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          101s

NAME                       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    101s

NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/main-app   1/1     1            1           101s

NAME                                 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-https

backend 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 不承载外部流量。
Last Modified: January 26, 2026
Archives Tip
QR Code for this page
Tipping QR Code