Container Orchestration/Kubernetes

Kubernetes에서 Source IP를 보존하는 방법

Somaz 2025. 5. 19. 10:54
728x90
반응형

Overview

Kubernetes 클러스터에서 외부로부터 들어오는 요청의 Source IP(클라이언트 IP) 를 정확히 파악하는 것은 로깅, 보안 분석, 감시(Audit), 접근 제어 등 다양한 운영 및 보안 목적에서 매우 중요하다.


그러나 Kubernetes의 기본 네트워크 구성에서는 서비스 타입에 따라 요청이 kube-proxy, NAT, ingress proxy 등을 거치며 Source IP가 변경되거나 소실되는 경우가 발생한다.

 

이 글에서는 Kubernetes 환경에서 Source IP가 왜 사라지는지, 그리고 이를 보존하기 위해 어떤 설정을 적용해야 하는지를 Service 타입별, Ingress 방식별로 정리한다.


또한 실습을 통해 실제 요청 시 IP가 어떻게 보이는지, 어떻게 보존할 수 있는지를 확인하는 과정을 Flask 앱과 MetalLB + Ingress-NGINX 조합을 이용한 실습을 통해 다룬다.

 

 

 

 


 

 

 

 

 

Service 타입별 Source IP 보존 여부

서비스 타입 설정 필요 여부 요청 전달 방식 Source IP 보존
ClusterIP 불가능 클러스터 내부 kube-proxy 통해 전달
NodePort / LoadBalancer externalTrafficPolicy: Local 설정 필요 클라이언트 → 노드 IP:포트 → 파드 직접 연결
Ingress X-Forwarded-For 헤더 사용 필요 클라이언트 → Ingress Controller → 파드 ✅ (헤더 기반)
MetalLB (LoadBalancer) externalTrafficPolicy: Local 설정 필요 외부 IP → 직접 노드 라우팅 (L2/BGP)
  • Ingress는 실제 IP가 파드까지 전달되지 않고, 헤더(X-Forwarded-For)로 전달되므로 애플리케이션에서 헤더 파싱이 필요하다.
 

 

 

왜 Cluster 모드에서는 Source IP가 사라질까?

Cluster 모드는 kube-proxy가 iptables 또는 ipvs로 트래픽을 전달하면서 SNAT(Source NAT) 처리를 수행하기 때문에, 요청자의 IP 대신 노드의 IP가 기록된다.

Local 모드는 SNAT 없이 로컬 노드에 있는 파드로 직접 전달하기 때문에 원본 IP가 보존된다.

 

 

 

 

 

`externalTrafficPolicy: Local` 사용하기

LoadBalancer 또는 NodePort 서비스에 다음과 같이 설정하면 클라이언트의 원래 IP를 유지할 수 있다.

apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  type: LoadBalancer
  externalTrafficPolicy: Local
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 8080
  • `Local`: 클라이언트의 원래 IP가 보존된다.
  • `Cluster`(기본값): 요청이 클러스터 내에서 NAT 처리되어 Source IP가 노드 IP로 바뀐다.

 

단, 해당 노드에 파드가 없으면 요청이 실패할 수 있으므로, DaemonSet 활용 또는 리소스 분산을 고려해야 한다.

 

 

 

 

 

 

Ingress(NGINX)에서 Source IP 보존하기

 

 

필수 조건 2가지

항목 설정 설명
Service 설정 `externalTrafficPolicy: Local` 클라이언트 IP가 SNAT되지 않고 그대로 전달되도록 합니다.
ConfigMap 설정 `use-forwarded-headers: "true"`
`proxy-real-ip-cidr: "0.0.0.0/0"`
전달받은 `X-Forwarded-For`, `X-Real-IP` 값을 신뢰해서 로그 및 백엔드로 넘긴다.

 

 

Configmap 설정

Ingress 컨트롤러(NGINX 등)는 기본적으로 L7 Proxy로 동작하기 때문에 Source IP가 변경된다. 이를 보존하려면 다음과 같이 설정한다.

apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-ingress-controller
data:
  use-forwarded-headers: "true"
  proxy-real-ip-cidr: 0.0.0.0/0

 

 

설명
`use-forwarded-headers: "true"` `X-Forwarded-For`, `X-Real-IP` 등 HTTP 헤더를 신뢰하고 사용하겠다는 의미이다.
`proxy-real-ip-cidr: 0.0.0.0/0` 어떤 클라이언트 IP든 신뢰하겠다는 의미입니다. 보통은 내부 네트워크 대역을 제한하는 게 보안상 좋다.

예: `10.0.0.0/8`, `192.168.0.0/16` 등.

이 설정은 NGINX가 `$proxy_protocol_addr`, `$remote_addr` 등을 올바르게 파싱할 수 있도록 해준다.

 

 

 

Service 설정

apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
spec:
  type: LoadBalancer # 또는 NodePort
  externalTrafficPolicy: Local
  ports:
    - port: 80
      targetPort: http
    - port: 443
      targetPort: https
  selector:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/component: controller

 

 

 

 

 

 


 

 

 

 

 

실습1: Source IP 확인 방법

 

 

1. flask 파드에 로그 설정 추가

apiVersion: v1
kind: Pod
metadata:
  name: flask-app
  labels:
    app: flask-app
spec:
  containers:
    - name: flask-app
      image: python:3.9-slim
      ports:
        - containerPort: 8080
      env:
        - name: PORT
          value: "8080"
      command: ["/bin/sh", "-c"]
      args:
        - |
          pip install flask
          mkdir -p /app
          cp /config/app.py /app/
          cd /app
          python app.py
      volumeMounts:
        - name: flask-app-config
          mountPath: /config
  volumes:
    - name: flask-app-config
      configMap:
        name: flask-app-config

 

 

 

2. Service 설정

apiVersion: v1
kind: Service
metadata:
  name: flask-app-service
spec:
  type: NodePort
  externalTrafficPolicy: Local
  selector:
    app: flask-app
  ports:
    - port: 8080
      targetPort: 8080

 

 

 

3. Configmap 설정

apiVersion: v1
kind: ConfigMap
metadata:
  name: flask-app-config
data:
  app.py: |
    from flask import Flask, request
    import os

    app = Flask(__name__)

    @app.route('/')
    def hello():
        client_ip = request.remote_addr
        x_forwarded_for = request.headers.get('X-Forwarded-For', '')
        x_real_ip = request.headers.get('X-Real-IP', '')
        all_headers = dict(request.headers)
        
        response = f"""
    Hello from Flask!
    
    Client Info:
    ------------
    Remote Addr: {client_ip}
    X-Forwarded-For: {x_forwarded_for}
    X-Real-IP: {x_real_ip}
    
    All Request Headers:
    -------------------
    {all_headers}
    """
        return response

    if __name__ == '__main__':
        app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 8080)))

 

 

 

ConfigMap 적용 포함한 Flask Pod + Service + ConfigMap 전체 구성

apiVersion: v1
kind: ConfigMap
metadata:
  name: flask-app-config
data:
  app.py: |
    from flask import Flask, request
    import os

    app = Flask(__name__)

    @app.route('/')
    def hello():
        client_ip = request.remote_addr
        x_forwarded_for = request.headers.get('X-Forwarded-For', '')
        x_real_ip = request.headers.get('X-Real-IP', '')
        all_headers = dict(request.headers)
        
        response = f"""
    Hello from Flask!
    
    Client Info:
    ------------
    Remote Addr: {client_ip}
    X-Forwarded-For: {x_forwarded_for}
    X-Real-IP: {x_real_ip}
    
    All Request Headers:
    -------------------
    {all_headers}
    """
        return response

    if __name__ == '__main__':
        app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 8080)))
---
apiVersion: v1
kind: Pod
metadata:
  name: flask-app
  labels:
    app: flask-app
spec:
  containers:
    - name: flask-app
      image: python:3.9-slim
      ports:
        - containerPort: 8080
      env:
        - name: PORT
          value: "8080"
      command: ["/bin/sh", "-c"]
      args:
        - |
          pip install flask
          mkdir -p /app
          cp /config/app.py /app/
          cd /app
          python app.py
      volumeMounts:
        - name: flask-app-config
          mountPath: /config
  volumes:
    - name: flask-app-config
      configMap:
        name: flask-app-config
---
apiVersion: v1
kind: Service
metadata:
  name: flask-app-service
spec:
  type: NodePort
  externalTrafficPolicy: Local
  selector:
    app: flask-app
  ports:
    - port: 8080
      targetPort: 8080

 

 

 

 

4. 외부에서 curl 요청

curl http://<NodeIP>:<NodePort>

 

 

 

 

5. 실습 확인

 

이제 확인해본다.

# 같은 대역대 예시

curl 10.10.10.51:30378

Hello from Flask!

Client Info:
------------
Remote Addr: 10.10.10.50
X-Forwarded-For: 
X-Real-IP: 

All Request Headers:
-------------------
{'Host': '10.10.10.51:30378', 'User-Agent': 'curl/7.81.0', 'Accept': '*/*'}


--------------------------------------------

# 다른 대역대 예시

curl 10.10.10.51:30378

Hello from Flask!

Client Info:
------------
Remote Addr: 10.10.10.251
X-Forwarded-For: 
X-Real-IP: 

All Request Headers:

 

 

 

현재 상황 요약

  • `externalTrafficPolicy: Local` 설정 
  • `NodePort` 로 워커 노드에 직접 접속 
  • FLASK 응답에 `$remote_addr` 는 보이고 같은 대역대에서는 source ip를 잘 보존한다.  다른 대역대에서는 실제 외부 IP가 아닌 게이트웨이 IP (예: 10.10.10.251) 로 나타남 ❌

 

 

 

원인: kube-proxy + NAT + Gateway ( 다른 대역대)

당신이 요청을 보낼 때, 요청이 물리적인 라우터 또는 가상 게이트웨이를 거쳐 오기 때문에 flask 파드에서는 실제 외부 클라이언트 IP가 아니라 게이트웨이 또는 NAT IP를 보게 된다.

[Your MacBook] 
     ↓ (요청)
[GW or NAT (10.10.10.251)]
     ↓ (SNAT됨)
[Worker Node (10.10.10.51):32440]
     ↓
[nginx Pod]
  • 결국 FLASK는 직접적인 클라이언트 IP가 아니라 SNAT된 IP (게이트웨이 IP) 를 받게 된다.

 

 

 

 

 

 


 

 

 

 

 

 

 

실습 2: MetalLB + Ingress-NGINX로 Source IP 보존

 

 

 

1. Metal LB 설정

Metal LB를 설치해준 뒤 진행해야 한다.

k get ipaddresspools.metallb.io -n metallb 
NAME      AUTO ASSIGN   AVOID BUGGY IPS   ADDRESSES
ip-pool   true          false             ["10.10.10.55-10.10.10.58","10.10.10.62-10.10.10.65"]

 

 

 

2. Ingress-NGINX Controller 설정

Test Ingress Nginx Controller를 Helm으로 생성해준다.

global:
  image:
    # -- Registry host to pull images from.
    registry: registry.k8s.io
controller:
  ingressClassResource:
    name: nginx-test  # 구분을 위한 새로운 이름
    enabled: true
    default: false     # 기본 컨트롤러가 아님을 명시
    controllerValue: "k8s.io/ingress-nginx-test"  # 구분을 위한 새로운 controller 값
  service:
    # -- Enable controller services or not. This does not influence the creation of either the admission webhook or the metrics service.
    enabled: true
    type: LoadBalancer
    loadBalancerIP: "10.10.10.63"
    externalTrafficPolicy: "Local"
  config:
    use-forwarded-headers: "true"
    real-ip-header: "X-Forwarded-For"
    set-real-ip-from: "0.0.0.0/0"
    real-ip-recursive: "true"

 

 

설치를 진행해준다.

helm install ingress-nginx-test . -n ingress-nginx -f ./values/mgmt-test.yaml

 

 

확인한다.

k get svc -n ingress-nginx ingress-nginx-test-controller -o yaml | grep external
  externalTrafficPolicy: Local
k get cm -n ingress-nginx ingress-nginx-test-controller -o yaml
apiVersion: v1
data:
  proxy-real-ip-cidr: 0.0.0.0/0
  use-forwarded-headers: "true"
..

 

 

 

3. nginx test.yaml 작성

apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-config
  namespace: default
data:
  nginx.conf: |
    events {}
    http {
      log_format main '$http_x_forwarded_for - $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;

      set_real_ip_from 0.0.0.0/0;
      real_ip_header X-Forwarded-For;
      real_ip_recursive on;

      server {
        listen 80;
        location / {
          return 200 'Hello from NGINX!';
        }
      }
    }
---
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  containers:
    - name: nginx
      image: nginx:latest
      ports:
        - containerPort: 80
      volumeMounts:
        - name: config
          mountPath: /etc/nginx/nginx.conf
          subPath: nginx.conf
  volumes:
    - name: config
      configMap:
        name: nginx-config
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  selector:
    app: nginx
  ports:
    - port: 80
      targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx-ingress
spec:
  ingressClassName: nginx-test
  rules:
    - host: test.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: nginx-service
                port:
                  number: 80

 

 

 

4. 테스트 방법

 

`/etc/hosts` 에 `test.local` 등록 (로컬 환경이라면)

echo "<LoadBalancer-IP> test.local" | sudo tee -a /etc/hosts

# 나의 경우
echo "10.10.10.63 test.local" | sudo tee -a /etc/hosts

 

 

접속 테스트

 

테스트 해본다.

# 외부망
curl http://test.local

Hello from Nginx!
Remote Addr: 10.233.65.165
X-Forwarded-For: 10.10.10.251


# 내부망
curl http://test.local
Hello from Nginx!
Remote Addr: 10.233.65.165
X-Forwarded-For: 10.10.10.60

 

 

curl 요청 시 -H "X-Forwarded-For: 1.2.3.4" 직접 지정해서 확인해본다.

curl -H "Host: test.local" -H "X-Forwarded-For: 1.2.3.4" http://10.10.10.63

 

 

nginx 로그에 1.2.3.4가 찍히면 nginx 파드가 real_ip_header 제대로 읽고 있다는 뜻이다.

결과는 아까 NodePort로 테스트 한것 과 동일하다.

 

 

 

 

 

 

 


 

 

 

 

 

 

 

마무리

Kubernetes에서 Source IP를 보존하는 것은 단순히 클라이언트 위치를 파악하기 위한 정보 수집 차원을 넘어,
보안 정책 수립, 공격자 추적, 서비스 최적화, 접근 제어, 트래픽 분석 등 다양한 운영 측면에서 필수적인 요소이다.

 

 

특히:

  • externalTrafficPolicy: Local 은 Kubernetes 서비스에서 SNAT을 피하고 원본 IP를 보존할 수 있는 가장 핵심적인 설정이다.
  • Ingress 환경에서는 X-Forwarded-For 와 real_ip_header 설정을 통해 간접적으로 원본 IP를 전달받을 수 있다.
  • MetalLB, Cilium 등 L2/L3 수준에서 Source IP를 정확히 전달할 수 있는 인프라 구성도 병행되어야 한다.

 

실제 운영 환경에서는 DaemonSet을 활용한 파드 분산, IP 신뢰 범위 제한(CIDR 필터링), 보안 고려 등을 함께 설계해야 하며,
불필요한 SNAT 처리를 최소화하고 클라이언트 식별의 신뢰성을 확보하는 것이 중요하다.

 

 

 

 

 

 

 

 

 

 

 


 

 

Reference

728x90
반응형