🏠 목록 Istio Egress Gateway 도입 가이드 — Passthrough vs mTLS Passthrough 📄 MD 원본 🌓 테마
istioegressmtlspassthroughadoptiondecisiontcpoperations

Istio Egress Gateway 도입 가이드 — Passthrough vs mTLS Passthrough

대상: 플랫폼/인프라/보안 담당자 환경: on-prem Kubernetes, Istio 1.30.0 (sidecar mode), PCI-DSS 준수 IDC 결정 요약: mTLS Passthrough(ISTIO_MUTUAL) 패턴 채택. proxy 도입에 따른 TCP 운영 이슈는 방식과 무관한 공통 비용이며 별도 완화 설정으로 대응함.


1. 배경 및 목표

[app + sidecar] ---> [egress gateway] ---> IDC FW ---> 대외기관 API
   (HTTPS 발신)        (유일한 출구)        (gw 대역만 허용)

2. 두 방안 비교

두 방식 모두 앱↔외부 서버의 종단간 TLS는 유지됨(gateway는 inner TLS를 복호화하지 않음). 차이는 sidecar→gateway 구간을 mesh mTLS로 한 겹 더 감싸는지 여부.

A. TLS Passthrough
   app --[원본 TLS]--> sidecar --[원본 TLS 그대로]--> gw --[원본 TLS]--> 외부
                                 gw는 SNI만 보고 통과 (신원 정보 없음)

B. mTLS Passthrough (ISTIO_MUTUAL)
   app --[원본 TLS]--> sidecar --[mesh mTLS[원본 TLS]]--> gw --[원본 TLS]--> 외부
                                 gw가 outer만 종단 -> SPIFFE 신원 추출
항목 A. Passthrough B. mTLS Passthrough
gateway에서 호출 주체 식별 불가 (pod IP뿐, IP는 휘발성) 가능 (SPIFFE: ns/sa 단위)
AuthorizationPolicy 표현력 목적지(SNI)·포트만 주체(principal) × 목적지
워크로드별 차등 통제 불가 — gateway 도달 가능한 모든 pod가 allowlist 합집합 사용 가능 — "app-a→A기관만" 식 최소권한
mesh 외부 클라이언트 차단 불가 (gateway Svc는 클러스터 전역 도달 가능) TLS handshake 단계에서 거부 (mesh CA 인증서 요구)
감사 로그 src pod IP SPIFFE ID (DOWNSTREAM_PEER_URI_SAN)
사고 시 즉시 회수 목적지 단위 전체 차단만 해당 SA 정책 1건 삭제
설정 복잡성 낮음 (DR 불필요, VS 양쪽 tls 라우트) 높음 (Gateway hosts ↔ DR sni ↔ VS 3자 정합)
디버깅 와이어에 원본 TLS 한 겹 mesh 구간만 TLS 2겹 (gw→외부는 단일 TLS로 동일)
연결 수립 레이턴시 기준 신규 연결당 outer 핸드셰이크 1회 추가
CPU 기준 sidecar(암호화)·gw(복호화) 증가
학습 곡선 낮음 라우트 타입 규칙, 인증서 체계 등 학습 필요

3. 결정: mTLS Passthrough 채택 근거

Passthrough의 결손은 구조적이며 운영 중 메울 수 없음.

mTLS의 비용은 인지하고 감수함.

참고: 두 모드는 같은 gateway Deployment에서 포트 단위로 공존 가능(예: 8443=ISTIO_MUTUAL, 9443=PASSTHROUGH). 필요 시 목적지별 점진 전환 경로 존재.


4. 아키텍처

ns: app-namespace                    ns: istio-egress
+------------------+                +-----------------------------------+
| app + sidecar    |  outer mTLS    | egressgateway Deployment (3+)     |
| (sa: app-a)  ----+--------------->| :8443 ISTIO_MUTUAL                |
+------------------+  Svc:443       |  1) tls_inspector: outer SNI로    |
                       -> pod:8443  |     filter chain 선택             |
                                    |  2) outer 종단 -> principal 추출  |
                                    |  3) AuthzPolicy(principal x sni)  |
                                    |  4) tcp_proxy -> 외부:443         |
                                    +-----------------+-----------------+
                                                      | (inner TLS만, 단일 겹)
                                                      v
                                          전용 노드풀 -> IDC FW -> 대외기관

핵심 동작 규칙 (트러블슈팅의 기준)

규칙 내용
라우트 타입 = 종단 여부 미종단 hop(sidecar, PASSTHROUGH gw) = tls 라우트(sniHosts 매칭) / 종단 hop(ISTIO_MUTUAL gw) = tcp 라우트
단일 포트 다중 목적지 목적지당 Gateway CR 1개(hosts로 분리) → istiod가 단일 리스너에 SNI별 filter chain 병합. CR 간 hosts 중복 금지
DR sni 필드 outer ClientHello의 SNI에 원본 호스트명 적재 → filter chain 선택 키. 누락 = handshake reset (최다 빈도 설정 오류)
거부 = HTTP 403 아님 L4 라우트이므로 RBAC 거부는 connection reset으로 나타남

5. 구성 방법 (목적지 1개 = 표준 1벌)

목적지 추가 시 ServiceEntry 항목 + Gateway CR + DR subset + VirtualService + AuthorizationPolicy 한 벌. Helm/ApplicationSet 템플릿화 대상.

# (0) ServiceEntry — 외부 호스트 레지스트리 등록 (공통, hosts에 추가)
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata: { name: external-apis, namespace: istio-egress }
spec:
  hosts: ["api.partner-a.example.com"]
  ports: [{ number: 443, name: tls, protocol: TLS }]
  resolution: DNS              # NONE이면 gw 자기 루프 발생
---
# (1) Gateway — 목적지당 1개. ISTIO_MUTUAL = mesh 인증서 요구
apiVersion: networking.istio.io/v1
kind: Gateway
metadata: { name: egress-partner-a, namespace: istio-egress }
spec:
  selector: { istio: egressgateway }
  servers:
  - port: { number: 8443, name: tls-partner-a, protocol: TLS }  # name은 CR마다 유일하게
    hosts: ["api.partner-a.example.com"]
    tls: { mode: ISTIO_MUTUAL }
---
# (2) DestinationRule — sidecar->gw outer mTLS + SNI 적재
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata: { name: egressgateway, namespace: istio-egress }
spec:
  host: egressgateway.istio-egress.svc.cluster.local
  subsets:
  - name: partner-a
    trafficPolicy:
      portLevelSettings:
      - port: { number: 443 }
        tls: { mode: ISTIO_MUTUAL, sni: api.partner-a.example.com }
        connectionPool:
          tcp:
            maxConnections: 4096
            tcpKeepalive: { time: 300, interval: 30, probes: 3 }
---
# (3) VirtualService — mesh쪽 tls / gateway쪽 tcp (종단 규칙)
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata: { name: partner-a-via-egress, namespace: istio-egress }
spec:
  hosts: ["api.partner-a.example.com"]
  gateways: [mesh, istio-egress/egress-partner-a]
  tls:
  - match:
    - { gateways: [mesh], port: 443, sniHosts: ["api.partner-a.example.com"] }
    route:
    - destination:
        host: egressgateway.istio-egress.svc.cluster.local
        subset: partner-a
        port: { number: 443 }
  tcp:
  - match:
    - { gateways: [istio-egress/egress-partner-a], port: 8443 }
    route:
    - destination: { host: api.partner-a.example.com, port: { number: 443 } }
---
# (4) AuthorizationPolicy — deny-all(1회) + 허용 1건 = 승인 1건
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata: { name: deny-all, namespace: istio-egress }
spec:
  selector: { matchLabels: { istio: egressgateway } }   # rules 없음 = 기본 거부
---
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata: { name: allow-app-a-to-partner-a, namespace: istio-egress }
spec:
  selector: { matchLabels: { istio: egressgateway } }
  action: ALLOW
  rules:
  - from: [{ source: { principals: ["cluster.local/ns/team-a/sa/app-a"] } }]
    when: [{ key: connection.sni, values: ["api.partner-a.example.com"] }]
---
# (5) 외부 호스트 DR — Envoy 기본 상한/keepalive 보정 (§6 참조)
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata: { name: partner-a-external, namespace: istio-egress }
spec:
  host: api.partner-a.example.com
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 4096        # 기본 1024 함정 제거
        connectTimeout: 3s
        idleTimeout: 1800s          # 채널 최장 유휴 간격보다 길게
        maxConnectionDuration: 3600s  # scale-out 후 재분배 유도 (민감 채널은 제외)
        tcpKeepalive: { time: 300, interval: 30, probes: 3 }  # FW idle timeout보다 짧게

Gateway Helm values 핵심 (배포 형상)

replicaCount: 3                       # 포트 공간 = 28k x replica
affinity:
  podAntiAffinity:                    # SNAT 환경에서 필수 (노드IP 포트 공간 공유 방지)
    requiredDuringSchedulingIgnoredDuringExecution:
    - labelSelector: { matchLabels: { istio: egressgateway } }
      topologyKey: kubernetes.io/hostname
nodeSelector: { node-role/egress: "true" }   # 전용 노드풀 = FW src IP 안정화
securityContext:
  sysctls:
  - { name: net.ipv4.ip_local_port_range, value: "10240 60999" }
  - { name: net.ipv4.tcp_tw_reuse, value: "1" }   # kubelet allowedUnsafeSysctls 필요
podAnnotations:
  proxy.istio.io/config: |
    terminationDrainDuration: 300s
    proxyMetadata: { EXIT_ON_ZERO_ACTIVE_CONNECTIONS: "true" }
service:
  type: ClusterIP
  ports:
  - { name: tls-egress, port: 443, targetPort: 8443 }

별도 적용: PodDisruptionBudget(minAvailable: 2).


6. 공통 고려사항 — L4 Proxy 도입에 따른 TCP 이슈

mTLS 채택 여부와 무관하게 적용되는 공통 비용. gateway는 tcp_proxy(L4)로 동작하므로 앱 연결 1개 = gw 소켓 2개의 1:1 매핑이며 연결 재사용이 원리적으로 불가(암호화된 바이트 파이프). 전사에 분산되어 있던 출발지 IP가 gw pod 소수로 수렴하는 집중 효과가 모든 이슈의 근원.

# 병목 한계 (기본값) 증상 완화
1 Envoy cluster 연결 상한 목적지당 1,024 초과분 즉시 거부, flag UO, upstream_cx_overflow 증가 외부 호스트 DR maxConnections 명시 (§5-(5))
2 Ephemeral port + TIME_WAIT gw pod 1개→목적지 1개: 동시 약 28k, 신규 약 470 conn/s (28,232포트÷60s) connect 실패, flag UF, EADDRNOTAVAIL 앱 keep-alive 가이드(근본), replica 분산+antiAffinity, ip_local_port_range 확장, tcp_tw_reuse=1
3 conntrack 테이블 노드 전역 약 26만 (TIME_WAIT류 120s 잔류) 무응답 timeout (silent drop), dmesg "table full" 노드 sysctl: nf_conntrack_max=1048576, tcp_timeout_time_wait=30
4 중간장비 idle timeout FW 보통 30~60분 half-open: 유휴 후 첫 송신에서 RST/timeout, 간헐적·재현 곤란 DR tcpKeepalive를 FW timeout보다 짧게. FW 값 사전 확인
5 수명 긴 연결 + 재배포 drain 기본 5s 배포 시 long-lived 연결 일괄 절단 terminationDrainDuration 연장, PDB, 배포 윈도우 정책

부딪히는 순서: 보통 1 → 2 → 3 (좁은 것부터). 4는 트래픽 적은 채널에서 먼저 발현.

도입 전 확인 체크리스트


7. 운영: 진단·모니터링

진단 순서 (구간별 1차 도구 — tcpdump는 최후 수단)

1) 거쳐갔는가?   gw access log (목적지 cluster + DOWNSTREAM_PEER_URI_SAN)
2) 왜 거부됐나?  istioctl proxy-config log --level rbac:debug -> principal 판정 확인
3) 설정 정합?    istioctl proxy-config listeners (filter chain SNI/cluster), istioctl analyze
4) 커널 레벨?    ss -tan state time-wait | wc -l, conntrack -C

reset 원인 분기 (클라이언트 증상이 모두 비슷하므로 필수 런북)

시그니처 원인
access log flag UO 연결 풀 초과 (병목 1)
access log flag UF gw→외부 connect 실패 (병목 2 또는 외부 장애)
rbac debug에 denied AuthorizationPolicy 거부 (정상 통제 동작)
gw 로그에 미도달 + sidecar에 PassthroughCluster mesh-side VS 미매칭 (sniHosts 오타/미주입)
handshake 즉시 실패 DR sni 누락/오타 또는 비-mesh 클라이언트 (정상 차단)
무응답 timeout conntrack drop (병목 3)
유휴 후 첫 요청 실패 half-open (병목 4)

알람 (Prometheus)

알람 조건 의미
연결 거부 rate(envoy_cluster_upstream_cx_overflow[5m]) > 0 maxConnections 도달
풀 사용률 upstream_cx_active / maxConnections > 0.8 선제 증설 신호
connect 실패 rate(envoy_cluster_upstream_cx_connect_fail[5m]) > 0 포트 고갈/외부 장애
TIME_WAIT egress 노드 node_sockstat_TCP_tw > 20000 churn 과다 → keep-alive 캠페인
conntrack entries / limit > 0.7 silent drop 임박

8. 전제조건 및 한계 (필독)

9. 참고 자료

참조 (아카이브)

이 가이드는 사내 공유본 원문 그대로다. 각 절의 메커니즘·실측·심화는 아카이브의 아래 문서로 분기한다.