Jaehong Jung

Istio에서 Client IP 특정하기 (Proxy Protocol v2)

kubernetesistioenvoyaws

AWS NLB + Istio Gateway 환경에서 클라이언트의 실제 IP를 획득하기 위한 구성 과정과, 그 과정에서 발생한 문제 및 해결 방법을 정리합니다.

기존 구성: ALB + Pod 직접 연결

기존 인프라에서는 ALB와 IP 기반 Target Group을 사용하고 있었습니다.

  • AWS Application Load Balancer (ALB)
  • Target Group: ip 모드 (protocol: HTTP)
  • 트래픽 흐름: Client → ALB → Pod

클라이언트 IP는 ALB의 X-Forwarded-For (Append) 옵션을 활성화하여 획득했습니다. 백엔드에서는 다음과 같은 방식으로 IP를 추출합니다.

// Pseudo-code
func getClientIP(req) string {
    if xff := req.Header["X-Forwarded-For"]; xff not empty {
        return xff[0] // 첫 번째 IP
    }
    return req.RemoteAddr // 없을 경우 fallback
}

변경된 구성: NLB + Istio Gateway

문제 상황

새로운 구성으로 전환하면서 클라이언트 IP를 획득할 수 없는 문제가 발생했습니다.

  • AWS NLB (Network Load Balancer)
  • Target Group: ip 모드 (protocol: TCP)
  • Istio Gateway (Envoy 기반)
  • 트래픽 흐름: Client → NLB → Istio Gateway (Envoy Proxy) → Pod

httpbin 등의 Pod에서 확인한 결과, X-Envoy-External-Address에 NLB의 IP 주소가 찍혔습니다. Istio Gateway에서 numTrustedProxies 값을 1, 2로 변경해도 효과가 없었고, XFF 헤더도 존재하지 않으며 RemoteAddr도 NLB 주소로 들어왔습니다.

해결 과정

1단계: NLB의 Preserve Client IP 설정 활성화

NLB Target Group의 Preserve Client IP address 옵션을 활성화했습니다.

설정 후 httpbin에서 X-Envoy-External-Address가 클라이언트의 실제 IP로 변경된 것을 확인했습니다. Envoy가 TCP의 Client IP를 확인하고 RemoteAddr에 설정하는 것으로 추정됩니다.

참고: postmanlabs/httpbin은 의도적으로 X-Forwarded-For 헤더를 드랍합니다. XFF를 확인하려면 mccutchen/go-httpbin을 사용하는 것이 좋습니다.

하지만 새로운 문제가 발생했습니다.

  • 일부 클라이언트 요청에서 응답 없음 (pending, empty response)
  • curl 및 브라우저 테스트에서 재현됨
  • Preserve Client IP 설정을 끄면 문제가 사라짐

원인 분석

Preserve Client IP 기능은 실제 패킷의 source IP를 클라이언트의 public IP로 유지합니다.

Client → NLB → Envoy까지의 인바운드 흐름은 정상이지만, Envoy가 응답을 보낼 때 문제가 발생합니다.

Preserve Client IP가 켜져 있으면 NLB는 source IP를 그대로 보존합니다. 즉, EC2(Pod)가 받는 패킷의 source IP가 클라이언트의 public IP(50.1.1.1)입니다.

flowchart LR
    Client["Client<br/>50.1.1.1"] -->|"Src: 50.1.1.1:51234<br/>Dst: NLB IP:Port"| NLB["NLB ENI<br/>10.1.1.10"]
    NLB -->|"Src: 50.1.1.1:51234<br/>Dst: 10.1.1.100:80<br/><b>source IP 보존!</b>"| EC2["EC2 (Pod)<br/>10.1.1.100"]

문제는 응답 경로에서 발생합니다. EC2가 응답을 보낼 때 destination IP가 클라이언트의 public IP(50.1.1.1)이므로, 반환 트래픽이 NLB를 거치지 않고 NAT Gateway를 통해 나갑니다.

flowchart TB
    subgraph VPC
        IGW["IGW"]
        subgraph Public Subnet
            NLB["NLB"]
            NATGW["NAT GW"]
        end
        subgraph Private Subnet
            EC2["EC2 (Pod)<br/>10.1.1.100"]
        end
    end
    Client["Client"] -->|"요청 (정상 인바운드)"| IGW
    IGW --> NLB
    NLB --> EC2
    EC2 -->|"응답: dst=50.1.1.1 (client)<br/>NLB를 거치지 않음!"| NATGW
    NATGW -->|"커넥션 깨짐 ✗"| Client

    style NATGW fill:#f66,stroke:#c00,color:#fff

EC2에서 보낸 응답 패킷의 destination이 클라이언트 public IP이므로, VPC 라우팅 테이블에 따라 NAT Gateway를 통해 나가게 됩니다. 클라이언트는 NLB를 통해 요청했지만 응답은 다른 경로로 오기 때문에 커넥션이 깨집니다.

최종 해결: Proxy Protocol v2 활성화

NLB Target Group에서 다음과 같이 설정합니다.

  • Preserve Client IP: OFF
  • Proxy Protocol v2: ON

Istio Gateway 설정에서도 Proxy Protocol support를 활성화해야 합니다.

동작 원리

Proxy Protocol은 TCP 접속 시 초기 핸드셰이크 구간에 Origin(client)의 IP 정보를 함께 전달합니다. Envoy는 이 Layer-4 정보를 활용하여 Client IP를 추출합니다.

실제 패킷은 NLB IP로 전달되므로 NAT 흐름에 영향을 주지 않으며, 원래의 source/destination IP 구조를 유지하면서 IP 정보를 별도의 layer에서 제공하는 방식입니다.

설정 조합별 결과 정리

Preserve Client IPProxy Protocol v2결과
OFFOFF클라이언트 IP 획득 불가능
ONOFF응답 불안정, Empty Response 발생
OFFON클라이언트 IP 정확히 수신 가능, 안정적인 통신 (헬스체크는 여전히 실패)
ONON여전히 Empty Response 발생

권장 설정

AWS NLB + Istio 사용 시 권장하는 설정입니다.

NLB Target Group:

  • Preserve Client IP: OFF
  • Proxy Protocol v2: ON
  • HealthCheck: 기존 HTTP port:15021/healthz/ready가 아닌 trafficPort로 TCP healthcheck로 대체

Istio Gateway (Envoy):

  • proxyProtocol 설정 ENABLE
  • numTrustedProxies: 필요 시 설정 (XFF를 함께 사용하는 경우)

Istio 설정

Proxy Protocol은 NLB Target Group에서만 활성화하는 것이 아니라, 트래픽을 받는 쪽(Envoy proxy)에서도 활성화해야 합니다.

configMap에서 Cluster 단위로 proxyProtocol을 enable하는 것은 Gateway TCP Listener의 경우에만 적용됩니다. HTTP listener를 사용하는 경우, Gateway Deployment의 annotations에 다음을 추가해야 합니다.

proxy.istio.io/config: '{"gatewayTopology": {"proxyProtocol": {}}}'

양쪽 모두 설정해야 정상 동작합니다.

targetGroup Proxy ProtocolIstio Proxy Protocol결과
ONOFFEmpty reply from server
OFFON400 Bad Request from envoy

참고: Envoy config에서 proxy protocol을 enable하면서 proxy protocol을 사용하지 않은 트래픽도 받아들일 수 있는 옵션이 있습니다.

Proxy Protocol v2 고려사항

NLB에 Proxy Protocol v2를 활성화할 때 두 가지를 고려해야 합니다.

  1. upstream gateway/proxy에서 Proxy Protocol을 지원해야 함 — NLB 뒤의 Envoy(Istio Gateway)가 Proxy Protocol 헤더를 파싱할 수 있어야 합니다. Istio의 경우 위에서 다룬 설정으로 해결됩니다.
  2. 성능 오버헤드 — TCP 핸드셰이크에 Proxy Protocol 헤더가 추가되므로 약간의 오버헤드가 발생할 수 있습니다.

성능 영향을 확인하기 위해 100 RPS GET 요청 기준으로 벤치마크를 진행했습니다.

Proxy Protocol v2 OFF (기존):

CPU (req/limit)Mem (req/limit)RPSp(90)p(95)p(99)
500m / 3000m1024Mi / 1024Mi1500~3.1ms~3.5ms~8.4ms
500m / 3000m1024Mi / 1024Mi2000~3.7ms~4.4ms~13ms

Proxy Protocol v2 ON:

CPU (req/limit)Mem (req/limit)RPSp(90)p(95)p(99)
500m / 3000m1024Mi / 1024Mi1500~3.7ms~4.4ms~9.8ms
500m / 3000m1024Mi / 1024Mi2000~3.5ms~4ms~7ms
500m / 3000m1024Mi / 1024Mi4000~4.4ms~6.3ms~13.9ms

동일 RPS 기준으로 레이턴시 차이는 미미했으며, 실 서비스에서 Proxy Protocol v2로 인한 성능 저하는 무시할 수 있는 수준이었습니다.

결론

기존 ALB 환경에서는 XFF 헤더 Append 방식으로 Client IP를 획득할 수 있었습니다. NLB + Istio 기반 환경에서는 TCP Target Group의 Proxy Protocol v2 기능을 활용하여 클라이언트 IP를 안정적으로 추출할 수 있습니다.

Preserve Client IP 기능은 커넥션 안정성 및 NAT 라우팅 문제로 인해 실 서비스에 적합하지 않았고, Proxy Protocol v2를 적용하여 L4에서 안정적으로 클라이언트 정보를 전달받을 수 있었습니다.