homelab89 Docs Logs Legacy Files ☰ TOC 🌓
guideistio 2026-06-10istioegressmtlspassthroughadoption

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. 배경 및 목표

  • 모든 대외(외부 기관) 통신을 통제된 단일 경로(egress gateway)로 수렴 필요.
  • 보안 요건: ① 외부행 경로 강제, ② 워크로드 단위 최소권한(어떤 앱이 어떤 목적지로 나가는지 차등 통제), ③ 주체 식별 가능한 감사 추적, ④ mesh 외부 클라이언트의 gateway 사용 차단.
  • 제약: 앱은 HTTPS를 직접 발신(종단간 TLS 유지 필요). 앱 코드/프로토콜 변경 불가.
[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의 결손은 구조적이며 운영 중 메울 수 없음.

  • 검문소(gateway)는 만들어지지만 “누가"를 판정할 재료가 없음. 한 목적지를 뚫는 순간 전사 모든 워크로드(비-mesh pod 포함)에 그 경로가 개방됨 — 최소권한 요건 미충족.
  • 감사 시점에 pod IP → 워크로드 역추적은 IP 휘발성 때문에 신뢰 불가 — 주체 식별 요건 미충족.
  • 추후 신원이 필요해지면 결국 ISTIO_MUTUAL 전환이 유일한 경로 → 지금의 구축 비용을 그 시점에 지불하게 됨.

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

  • 비용: 신규 연결 수립 레이턴시 증가(핸드셰이크 1회분), 암호화 CPU 부하, 초기 설정 복잡성과 학습 곡선, mesh 구간 TLS-in-TLS 디버깅.
  • 감수 근거: 이 비용은 설계 시점에 1회 지불되고 템플릿에 동결됨(목적지 추가 = 표준 1벌 복사). 반면 passthrough의 결손은 런타임에 상존하며 사고·심사 시점에 청구됨. 가시성과 트래픽 통제가 비용을 상회한다고 판단.
  • 디버깅 부담의 실제 범위: 장애 다발 구간인 gw→외부는 단일 TLS로 passthrough와 동일. 이중 TLS는 mesh 내부 hop에 한정되며, 해당 구간 1차 진단 도구는 tcpdump가 아닌 access log / istioctl로 표준화(§7).

참고: 두 모드는 같은 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는 트래픽 적은 채널에서 먼저 발현.

도입 전 확인 체크리스트

  • SNAT 여부 (Calico natOutgoing): on이면 외부가 보는 src = 노드 IP → 같은 노드 gw pod들이 포트 공간 공유 + FW 등록 단위 변경. 포트 예산·방화벽 신청을 동시에 결정하는 1순위 항목
  • 목적지별 peak 동시 연결 수·conn/s 측정 (istio_tcp_connections_opened_total) → 캐파 산정 입력값
  • gw↔외부 구간 FW idle timeout 값
  • 채널별 연결 성격 분류: short-lived(포트 churn 위험) vs long-lived(drain·half-open 위험)
  • 상대 기관에 src IP 수렴 사전 공지 (IP 기반 rate limit/이상탐지 대비)
  • 앱팀 대상 connection pool/keep-alive 가이드 배포 (passthrough 구간이라 gw가 강제 불가)
  • istiod HA 점검: 인증서 로테이션(24h TTL)이 egress 가용성에 직결

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. 전제조건 및 한계 (필독)

  • egress gateway 자체는 보안 경계가 아님 (Istio 공식 문서 명시). 침해된 pod의 sidecar 우회를 Istio는 막을 수 없음. 다음 3중 통제가 도입의 전제:
    1. CNI NetworkPolicy: 일반 워크로드 external egress default-deny, egress gw + DNS만 허용
    2. 전용 노드풀 + IDC 방화벽: 외부행은 egress 노드 대역만 허용
    3. Istio outboundTrafficPolicy: REGISTRY_ONLY (보조 수단, best-effort)
  • SA 위생이 신원 모델의 전제: 워크로드당 전용 ServiceAccount 필수. default SA 공유 시 신원 통제 무의미 (Kyverno 정책으로 강제 권장)
  • AuthorizationPolicy 표현 범위는 connection 레벨(principal, SNI, 포트)까지. HTTP path/method 통제는 불가 (passthrough 구조의 본질적 한계 — 보안 요건 협의 시 명시)
  • SNI는 클라이언트 제공 값. inner TLS의 서버 인증서 검증은 앱 책임으로 유지됨을 정책 문서화

9. 참고 자료

참조 (아카이브)

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


검증 기록 (2026-07-05 · Istio 1.30.0 / k8s 1.30.6)

검증 방법: 공식 Istio/Envoy 문서·GitHub 이슈 대조 + homelab 클러스터(k8s 1.30.6, Istio 1.30.0) 실측 2건(T11, T48).

주장 판정 근거
C1. Passthrough·mTLS Passthrough 모두 앱-외부 종단간 TLS 유지, gw는 inner TLS 미복호화 ✅ 문헌 확인 istio.io/latest/blog/2023/egress-sni
C2. ISTIO_MUTUAL은 outer mesh mTLS만 종단해 SPIFFE identity 추출, inner TLS는 통과 ✅ 문헌 확인 …/reference/config/networking/gateway
C3. ISTIO_MUTUAL gw는 mesh 인증서 없는 비-mesh 호출자를 handshake 단계에서 차단 ✅ 실측 확인 …/reference/config/networking/gateway · T46 실측
C4. 미종단 hop=tls: 라우트, 종단 hop(ISTIO_MUTUAL)=tcp: 라우트 — 종단 리스너에 tls: 걸면 미동작 ✅ 문헌 확인 github.com/istio/istio#37293
C5. DR TLSSettings의 sni 필드는 목적지별 ClientHello SNI를 실어 filter chain을 선택하는 용도 ✅ 문헌 확인 …/reference/config/networking/destination-rule
C6. 목적지당 Gateway CR 분리 → istiod가 단일 리스너 filter chain으로 병합, hosts 중복 금지 ✅ 문헌 확인 github.com/istio/istio#13717
C7. ServiceEntry resolution: NONE은 gw 자기 루프 유발 — DNS 사용 필요 ✅ 문헌 확인 …/tasks/traffic-management/egress/egress-gateway
C8. L4(passthrough) 경로에서 AuthorizationPolicy 거부는 HTTP 403이 아니라 connection reset ✅ 실측 확인 envoyproxy.io/…/rbac_filter · T11 실측
C9. Envoy cluster 기본 연결 상한(max_connections) 1,024, 초과 시 upstream_cx_overflow ✅ 문헌 확인 envoyproxy.io/…/circuit_breaker.proto
C10. Istio 프록시 기본 terminationDrainDuration 5초 ✅ 문헌 확인 …/reference/config/istio.mesh.v1alpha1#ProxyConfig
C11. istiod 발급 워크로드 mTLS 인증서 기본 TTL 24시간 ✅ 문헌 확인 istio.io/v1.6/faq/security
C12. egress gateway만으로는 안전한 강제 경계가 안 됨 — NetworkPolicy/방화벽 등 외부 통제 필요 ✅ 문헌 확인 …/tasks/traffic-management/egress/egress-gateway
C13. outboundTrafficPolicy: REGISTRY_ONLY는 강한 보안 경계가 아닌 best-effort ✅ 문헌 확인 …/ops/best-practices/security
C14. Linux 기본 ephemeral 포트(32768-60999)·TIME_WAIT 60s 기준 신규 연결 한계 어림계산 ≈470 conn/s ✅ 문헌 확인 docs.kernel.org/networking/ip-sysctl
C15. egress gw는 tcp_proxy(L4)로 동작 — 다운스트림 연결 1개=업스트림 소켓 1개(1:1), 서로 다른 연결 간 재사용 불가 ✅ 실측 확인 envoyproxy.io/…/connection_pooling · T48 실측

Files