homelab89 Docs Logs Legacy Files ☰ TOC 🌓
noteistio 2026-06-08egressmtlssnispiffeenvoy

Test Report — Egress Gateway "HTTPS over mTLS" (ISTIO_MUTUAL)

ABSTRACT

egress gateway에서 sidecar↔gw 구간만 Istio mTLS(ISTIO_MUTUAL)로 감싸 게이트웨이가 호출자의 SPIFFE 신원을 검증하면서, 앱이 보낸 HTTPS는 외부까지 end-to-end로 보존되는 “이중 TLS” 패턴을 홈랩에서 실측 검증한 리포트. 결론: 동작하지만(200), 처음 manifest 그대로는 깨졌고 그 실패 두 개가 이 패턴의 핵심 원리를 그대로 드러낸다 — 종단하면 SNI가 소비된다는 한 문장에서 모든 설정 결정과 두 함정이 따라 나온다.

Date: 2026-06-08 · Cluster: homelab (kubespray bare-metal, k8s v1.30.6, Calico) · Istio: 1.30.0 (istiod + egress gateway, Helm) Scenario: scenarios/20-egress/*-cnn-mtls 세트 · NS: mesh-test (istio-injection=enabled) / 외부: edition.cnn.com 대상독자: egress 패턴은 한 번 봤지만 ISTIO_MUTUAL과 PASSTHROUGH가 다른 라우트 타입을 강요하는지 궁금한 SRE · 선행: egress gateway 개념, mesh mTLS/SPIFFE, TLS SNI


0. 배경 지식 — PASSTHROUGH는 “누가 나가는지"를 모른다

egress gateway의 기본형은 HTTPS PASSTHROUGH다(이 아카이브의 control: httpbin.org). 게이트웨이가 TLS를 풀지 않고 단일 TLS 레이어의 SNI만 읽어 외부로 포워딩한다. 구현은 단순하고 앱 TLS가 end-to-end로 보존되는 장점이 있지만, 보안상 빈 칸이 하나 있다: 메시 내부 leg(sidecar→gw)가 평문 TCP(그 위에 앱 TLS만 얹힘)다. 게이트웨이 입장에서 들어온 바이트에는 호출 워크로드의 신원이 없다. 즉 “누가 외부로 나가는가"를 암호학적으로 식별하지 못한다. egress를 보안 경계로 쓰려면 이게 치명적이다 — 신원 없이는 그 위에 인가(authorization)도 못 건다.

이 빈 칸을 메우는 변형이 “HTTPS over mTLS"다. in-mesh leg를 메시 mTLS(ISTIO_MUTUAL)로 묶어 게이트웨이가 SPIFFE 신원을 검증하게 하되, 앱은 평문 HTTP가 아니라 HTTPS를 그대로 보낸다. 결과는 두 개의 TLS 레이어가 중첩된 구조 — 바깥은 mesh mTLS(신원), 안쪽은 앱 HTTPS(기밀성).

전제 개념 셋만 잡고 가면 된다.

개념 한 줄 정의 이 리포트에서의 역할
TLS termination vs passthrough 게이트웨이가 TLS를 푸는가(terminate) 그냥 흘리는가(passthrough) 둘의 차이가 라우트 타입(tcp vs tls)을 강제 — 함정 2의 뿌리
SNI TLS handshake가 평문으로 싣는 목적지 호스트명 passthrough면 라우팅 키, terminate면 풀리는 순간 소비되어 다음 leg에선 못 씀
SPIFFE / mesh mTLS sidecar가 제시하는 워크로드 신원 인증서(spiffe://…/sa/…) egress-gw가 누가 호출했는지 검증하는 근거 → mTLS/SPIFFE 신원

비교 기준선: Ingress/Egress 검증 리포트 (PASSTHROUGH control) · 운영 주의점 Egress 운영 가이드 · 필드 매뉴얼 src-egress-gateway


1. 핵심 아키텍처 — “종단하면 SNI가 소비된다”

머릿속에 둘 그림 하나: egress-gw에서 바깥 TLS는 종단되고 안쪽 TLS는 통과한다. 바깥(mesh mTLS)을 푸는 순간 그 handshake가 싣고 온 SNI는 그 자리에서 소비된다. 그래서 표준 Gateway/VirtualService API만 쓴다면 그 뒤 leg-2는 더 이상 SNI로 라우팅할 수 없고, 불투명한 바이트(tcp proxy) 로만 흘려야 한다(단일 외부 호스트만 다루는 이 리포트의 범위에서는 이걸로 충분하다 — 여러 호스트를 SNI로 다시 구분해야 하는 경우의 예외는 §핵심 정리 참고). 이 한 문장이 아래 모든 설정과 두 함정의 원천이다.

flowchart LR
  app["sleep app<br/>curl https://edition.cnn.com"]
  sc["sidecar<br/>(envoy)"]
  egw["egress-gw<br/>listener :15443"]
  cnn["edition.cnn.com:443"]

  app -->|"app TLS (inner)"| sc
  sc -->|"outer: mesh mTLS (ISTIO_MUTUAL)<br/>SNI=edition.cnn.com + client cert"| egw
  egw -->|"terminate outer / verify SPIFFE<br/>then tcp_proxy inner bytes"| cnn
  app -. "inner app TLS stays end-to-end (gw never decrypts)" .-> cnn

두 레이어를 각각 보면:

  • outer (sidecar↔egress) = Istio mTLS. sidecar가 SPIFFE client cert를 제시하고, egress-gw가 그걸 강제(requireClientCertificate)하며 mesh CA로 검증한다. 여기서 누가 나가는지가 결정된다.
  • inner (app↔cnn) = 앱 HTTPS. egress-gw는 바깥 mesh TLS만 풀고 안쪽 앱 TLS는 복호화하지 않은 채 tcp_proxy로 cnn까지 전달. 그래서 게이트웨이는 끝까지 평문 payload를 못 본다.

왜 PASSTHROUGH와 ISTIO_MUTUAL이 라우트 타입을 가르나

본질은 “종단(terminate) 여부” 한 축이다. 이게 SNI의 운명을 정하고, SNI의 운명이 leg-2의 라우트 타입을 정한다.

outer TLS를 푸는가 leg-1에서 SNI는 leg-2 라우팅 키 leg-2 라우트 타입
PASSTHROUGH 안 푼다 (그냥 흘림) 끝까지 살아 있음 SNI(sniHosts) tls
ISTIO_MUTUAL 푼다 (mesh mTLS 종단·SPIFFE 검증) 푸는 순간 소비됨 더 못 씀 → 포트 매칭 tcp (tcp_proxy)

PASSTHROUGH는 게이트웨이가 SNI를 끝까지 들고 갈 수 있으니 leg-2도 tls/sniHosts로 받는다. ISTIO_MUTUAL은 종단하는 순간 SNI가 사라지므로, 종단된 listener에는 SNI 기반 network filter가 하나도 안 생긴다. Envoy listener는 filter chain이 0개면 통째로 omit된다(함정 2). 그래서 leg-2는 SNI를 포기하고 포트 매칭 + tcp 라우트로 받아 안쪽 바이트를 그대로 흘린다. — 자세한 설정 대비는 HTTP vs HTTPS egress 설정, 구조 정본은 HTTPS over mTLS 구조.


2. CRD 구성 — 4 리소스 = 4개의 질문 + 정렬 지도

별도 도메인 edition.cnn.com을 써서 기존 httpbin PASSTHROUGH를 control로 보존하고 직접 비교한다. 리소스 4종을 “그게 답하는 질문"으로 보면 길을 안 잃는다.

파일 (리소스) 답하는 질문 PASSTHROUGH 대비 델타
serviceentry-cnn-ext.yaml “이 외부 호스트를 메시가 인지하는가?” 도메인만 다름 (protocol TLS / resolution DNS)
gateway-egress-cnn-mtls.yaml “게이트웨이는 무엇을 어느 포트로 받고, 종단하는가?” port 15443 / tls.mode: ISTIO_MUTUAL (← PASSTHROUGH, 포트 분리)
destinationrule-egress-cnn-mtls.yaml “sidecar는 gw로 갈 때 무엇으로 감싸는가?” portLevelSettings.tls {ISTIO_MUTUAL, sni} 신규 (passthrough DR은 subset만)
virtualservice-egress-cnn-mtls.yaml “트래픽을 어떻게 gw 경유로 강제하는가?” leg-2가 tcp (passthrough는 tls/sniHosts)

정렬 지도 — 같은 magic string이 어디서 어디로 묶이나

세 리소스가 두 개의 magic value를 공유한다. 하나라도 어긋나면 listener 매칭이 깨져 연결이 끊긴다.

포트 15443    : Gateway.server.port.number  ==  DR.portLevelSettings.port.number  ==  VS leg-1 destination.port  ==  VS leg-2 match.port
SNI cnn.com   : DR.tls.sni                  ==  Gateway.server.hosts[0]            (sidecar가 거는 mesh-mTLS SNI == gw server가 필터체인 매칭하는 키)
subset  cnn   : DR.subsets[].name           ==  VS leg-1 destination.subset

핵심은 DR의 sni: sidecar가 mesh mTLS handshake에 싣는 SNI를 외부 도메인으로 강제 설정하고, egress-gw의 Gateway.server.hosts그 SNI로 필터체인을 고른다. 이 sni가 없거나 어긋나면 게이트웨이 listener가 매칭에 실패해 그냥 끊긴다.

적용한 YAML 전체 (주석 = 줄마다 왜)

apply 그대로의 4개 파일. 적용 당시 기준 apiVersion은 networking.istio.io/v1beta1이다. (2026-07 정정: Istio 1.22(2024-05)부터 networking.istio.io/v1이 GA로 승격되어 1.30 기준으로는 v1이 신규 작성 권장 apiVersion이다 — v1beta1은 폐기되지 않아 계속 동작하므로 아래 매니페스트가 깨지는 것은 아니다.)

# ServiceEntry — 외부 도메인을 메시 레지스트리에 등록. 등록해야 sidecar/egress-gw가
#   이 호스트로의 트래픽을 인지하고 VS/DR을 걸 수 있다. 앱이 end-to-end TLS를 유지하므로
#   protocol TLS(복호화 안 함), resolution DNS. (httpbin passthrough와 충돌 없게 별도 도메인)
apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
metadata: { name: cnn-ext, namespace: mesh-test }
spec:
  hosts: [edition.cnn.com]
  ports:
    - { number: 443, name: tls, protocol: TLS }
  resolution: DNS
  location: MESH_EXTERNAL
---
# Gateway — egress-gw가 15443에서 받고 ISTIO_MUTUAL로 종단·SPIFFE 검증.
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata: { name: egress-cnn, namespace: mesh-test }
spec:
  selector: { istio: egressgateway }   # istio-system egress gw pod 라벨
  servers:
    - port:
        number: 15443        # 443(passthrough/httpbin)과 분리한 별도 tls 포트 → 함정 1
        name: tls-cnn
        protocol: TLS
      hosts: [edition.cnn.com]   # 이 호스트명 == DR.tls.sni (필터체인 매칭 키)
      tls:
        mode: ISTIO_MUTUAL   # 메시 mTLS 종단 + client SPIFFE 검증 (vs PASSTHROUGH)
---
# DestinationRule — sidecar→gw 구간에 ISTIO_MUTUAL을 거는 핵심. passthrough DR은 subset만,
#   여기선 trafficPolicy가 반드시 필요하다.
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata: { name: egressgateway-cnn, namespace: mesh-test }
spec:
  host: istio-egressgateway.istio-system.svc.cluster.local
  subsets:
    - name: cnn
      trafficPolicy:
        loadBalancer: { simple: ROUND_ROBIN }
        portLevelSettings:
          - port: { number: 15443 }      # gw 수신 포트(Gateway server와 일치)
            tls:
              mode: ISTIO_MUTUAL          # sidecar가 SPIFFE client cert 제시
              sni: edition.cnn.com        # gw server SNI 매칭 키 (어긋나면 연결 끊김)
---
# VirtualService — 2단 라우팅으로 cnn 호출을 egress-gw 경유로 강제.
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata: { name: egress-cnn, namespace: mesh-test }
spec:
  hosts: [edition.cnn.com]
  gateways:
    - mesh          # 1단: sidecar에서 나오는 트래픽
    - egress-cnn    # 2단: egress gateway에서 나오는 트래픽
  tls:
    # 1단: sidecar → egress-gw(subset cnn). 종단 안 함 → tls/sniHosts로 라우팅.
    #   match port 443 = 앱이 cnn:443 연결. destination 15443 = gw 수신.
    - match:
        - { gateways: [mesh], port: 443, sniHosts: [edition.cnn.com] }
      route:
        - destination:
            host: istio-egressgateway.istio-system.svc.cluster.local
            subset: cnn
            port: { number: 15443 }
  tcp:
    # 2단: gw가 mesh mTLS 종단 후 → 안쪽 앱 TLS를 tcp_proxy로 cnn:443에 전달.
    #   종단 listener라 SNI는 이미 소비됨 → 포트 매칭만(listener에 호스트 1개라 충분) → 함정 2.
    - match:
        - { gateways: [egress-cnn], port: 15443 }
      route:
        - destination: { host: edition.cnn.com, port: { number: 443 } }
          weight: 100

3. 두 함정 (실측 → 교정)

처음 작성한 manifest는 (a) 443 그대로, (b) leg-2도 tls/sniHosts였다. 둘 다 §1 anchor가 예고한 대로 깨졌다.

flowchart TD
  s0["manifest v1<br/>(443, leg-2 = tls/sniHosts)"] --> p1
  p1{"443 = httpbin PASSTHROUGH 점유 중"} -->|"머지 충돌<br/>cnn 서버 드롭"| e1["curl: Connection reset<br/>egw log: NR filter_chain_not_found"]
  e1 --> f1["FIX 1: 포트 15443 분리"]
  f1 --> p2{"ISTIO_MUTUAL = 메시 mTLS 종단"}
  p2 -->|"종단 listener에 tls/sniHosts<br/>= network filter 0개"| e2["istiod: omitting listener<br/>must have more than 0 chains"]
  e2 --> f2["FIX 2: leg-2를 tcp 라우트로"]
  f2 --> ok["listener 생성<br/>:15443 SNI edition.cnn.com → outbound cnn:443"]

함정 1 — 포트 충돌 (당시 환경에서 관측된 드롭 증상). 같은 egress-gw에 이미 443(PASSTHROUGH, httpbin)이 server를 점유 중인 상태에서 같은 443에 ISTIO_MUTUAL server(cnn)를 또 얹었더니, cnn 쪽 filter chain이 통째로 사라져 NR filter_chain_not_found + Connection reset이 발생했다. 2026-07 실측 정정(T08): 이후 homelab에서 동일 포트에 PASSTHROUGH server와 ISTIO_MUTUAL server를 호스트(SNI)만 다르게 공존시켜본 결과, filter chain이 2개(SNI별로 분리된 filterChainMatch.serverNames)로 정상 생성되고 conflict/duplicate 로그 없이 각자 정확히 라우팅됐다. 즉 “TLS를 안 푸는 server와 종단하는 server를 같은 포트에 두면 반드시 한쪽이 드롭된다"는 인과는 Istio의 일반 아키텍처 제약이 아니다 — Envoy는 원래 SNI 기반 filter chain을 같은 물리 포트에 여러 개 공존시킬 수 있다. 이 리포트에서 실제로 관측된 드롭은 이 일반 법칙 때문이라기보다 당시 구성의 다른 디테일(예: 두 server의 host 매칭이 실제로는 겹쳤을 가능성)에서 비롯됐을 개연성이 높다 — 다만 결과적으로 택한 해법인 egress-gw Service가 노출하는 별도 tls 포트 15443(→targetPort 15443)으로 server를 분리하는 것 자체는 여전히 안전하고 유효한 선택이다.

함정 2 — 종단 listener엔 SNI 라우트가 안 먹는다. ISTIO_MUTUAL은 mesh mTLS를 종단한다(§1). 종단되면 SNI가 소비되므로 leg-2의 tls/sniHosts 매칭은 만들 network filter가 없어, 그 listener가 통째로 omit된다:

warn  gateway mesh-test/egress-cnn:15443 listener missed network filter
info  gateway omitting listener "0.0.0.0_15443" due to: must have more than 0 chains

→ 종단 후 안쪽(앱 TLS) 바이트를 흘리려면 leg-2를 tcp 라우트(tcp_proxy) 로 받는다. 이게 §1 표의 ISTIO_MUTUAL 행 그대로다.


4. 예시와 결과 — 4개 테스트로 두 레이어를 각각 입증

검증 전략: 200 한 줄로는 부족하다. 바깥 leg가 정말 mTLS인가, 안쪽 leg가 정말 end-to-end인가를 각각 따로 봐야 “이중 TLS"가 입증된다. TEST 1·2는 결과(통신·종단), TEST 3·4는 두 레이어의 설정 증거.

TEST 1 — 외부 호출 성공 + 앱 TLS end-to-end

kubectl -n mesh-test exec deploy/sleep -c sleep -- \
  curl -sS -o /dev/null -w "HTTP=%{http_code} ssl_verify=%{ssl_verify_result} remote=%{remote_ip}:%{remote_port}\n" \
  https://edition.cnn.com/
# 실제: HTTP=200  ssl_verify=0  remote=151.101.195.5:443   ← 앱 TLS end-to-end 검증 OK

ssl_verify=0이 핵심이다 — curl(앱)이 cnn의 인증서를 직접 검증해서 통과했다는 뜻. 게이트웨이가 안쪽 TLS를 풀었다면 cnn 인증서를 curl이 못 봤을 것이다. 즉 안쪽 레이어가 정말 end-to-end다.

TEST 2 — egress 경유 + 종단 성공 (egress-gw access log)

[11:14:34] 0 - - ... 2422 4731478 435 ... "151.101.3.5:443" outbound|443||edition.cnn.com
           10.255.126.47:55322 10.255.126.47:15443 ... edition.cnn.com

NR filter_chain_not_found 사라짐, response flag -(성공), 4.7MB(4731478B) 수신 — cnn 페이지가 tcp_proxy를 통해 그대로 흘러왔다는 증거. 목적지 cluster는 outbound|443||edition.cnn.com.

TEST 3 — in-mesh leg가 mTLS인가 (egress-gw 15443 listener의 바깥 레이어)

# :15443 listener의 filter chain TLS 컨텍스트에서 아래 필드 확인
istioctl proxy-config listener deploy/istio-egressgateway.istio-system --port 15443 -o json \
  | grep -E 'sni|requireClientCertificate|validationContext|secretName'
SNI match                  : ['edition.cnn.com']
requireClientCertificate   : True       ← client cert 강제(메시 mTLS)
validationContext(SPIFFE)  : True       ← mesh CA로 client 신원 검증
server cert SDS            : ['default'] ← 게이트웨이도 자기 메시 신원 제시

requireClientCertificate=True + validationContext=True = 게이트웨이가 누가 호출했는지 SPIFFE로 검증하도록 켜져 있다 = 바깥 레이어가 정말 mTLS다.

TEST 4 — sidecar leg-1이 mTLS 래핑 + sni 적용 (sleep cluster)

cluster outbound|15443|cnn|...egressgateway
  transportSocket: envoy.transport_sockets.tls
  sni: edition.cnn.com          ← DR의 sni 적용됨(게이트웨이 SNI 매칭 키)
  clientCertSDS: ['default']    ← sleep이 자기 메시 신원 client cert 제시

cluster 이름 outbound|15443|cnn|...egressgateway(direction|port|subset|fqdn)가 DR이 의도한 대로 15443·subset cnn으로 떴고, sni: edition.cnn.com이 §2 정렬 지도의 DR.tls.sni == Gateway.server.hosts[0] 매칭을 sidecar 쪽에서 입증한다.

합격 판정

합격선 결과 무엇을 증명
외부 호출 200 ✅ HTTP=200, ssl_verify=0 경로 전체 동작
egress gateway 경유 ✅ :15443 수신 → outbound cnn gw 강제 경유
in-mesh leg = 메시 mTLS(client cert 강제+검증) ✅ requireClientCertificate + validationContext 바깥 레이어 = 신원
외부 leg = 앱 TLS end-to-end(게이트웨이 미복호화) ✅ tcp_proxy, ssl_verify=0 안쪽 레이어 = 기밀성
기존 httpbin passthrough(control) 무손상 ✅ 간헐 200(503/timeout은 외부 flakiness + outlier DR) 회귀 없음

핵심 정리

  • 모든 게 “종단 여부” 한 축에서 갈린다. PASSTHROUGH=안 푼다(SNI 끝까지 살아 라우팅·L4 관측만). ISTIO_MUTUAL=바깥 mesh TLS를 종단·SPIFFE 검증 후 안쪽을 다시 흘린다. 종단하면 SNI가 소비되므로, 표준 Gateway/VirtualService API만으로는 leg-2를 tls(SNI)가 아니라 tcp(tcp_proxy) 로 받아야 한다 — 단 이는 절대 법칙이 아니라 표준 API의 한계다. Istio 공식 블로그(egress-sni, 2023)는 EnvoyFilter로 내부(internal) listener + TLS inspector + dynamic_forward_proxy를 추가해 종단 이후에도 inner TLS의 SNI를 다시 읽어 여러 외부 호스트로 SNI 기반 라우팅하는 패턴을 제시한다. 이 리포트처럼 단일 호스트만 다룬다면 tcp 라우트로 충분하지만, 여러 호스트를 SNI로 다시 구분해야 한다면 EnvoyFilter가 필요하다.
  • “HTTPS over mTLS"는 이중 TLS다. 바깥 = mesh mTLS(누가=신원), 안쪽 = 앱 HTTPS(무엇=기밀성, gw 미복호화). 게이트웨이는 안쪽을 끝까지 못 본다.
  • 한 egress-gw에서 PASSTHROUGH와 TLS-terminate를 포트 분리로 안전하게 공존시켰다 — PASSTHROUGH=443, ISTIO_MUTUAL=15443. (실측 정정: 이 리포트는 같은 포트 병합이 반드시 한쪽을 드롭시킨다고 서술했으나, 이후 homelab 실측(T08)에서는 host/SNI가 다르면 같은 포트에서도 filter chain 2개로 정상 공존함이 확인됐다 — 포트 분리는 여전히 안전하고 무난한 선택이지만 아키텍처적 필연은 아니다.)
  • 정렬 지도가 깨지면 조용히 끊긴다. 15443(Gateway·DR·VS 양 leg), sni: edition.cnn.com(DR==Gateway.hosts), subset cnn(DR==VS leg-1)이 모두 일치해야 listener가 매칭된다.
  • 검증선 = 두 레이어를 따로: requireClientCertificate=True+validationContext=True(바깥=신원) · ssl_verify=0+tcp_proxy(안쪽=end-to-end) · access log response flag -(종단·전달 성공).

What you might be missing

  • 이 패턴은 “식별"이지 “인가"가 아니다. egress-gw는 누가 나가는지 SPIFFE로 식별만 한다 — 허용 여부는 강제하지 않는다. 신원이 검증돼도 cnn으로 나가는 건 막히지 않는다. “특정 SA만 cnn으로"라는 인가는 egress-gw에 AuthorizationPolicy(principals 기반)를 따로 걸어야 한다. → AuthorizationPolicy 멘탈모델
  • 이중 TLS면 게이트웨이는 L7을 못 본다. 안쪽 앱 TLS를 안 푸니 관측은 L4(istio_tcp_*)뿐. HTTP path·method 등 L7을 보려면 앱이 평문 HTTP를 보내고 게이트웨이가 외부로 TLS를 새로 거는 TLS origination이라는 다른 패턴이어야 한다(이 리포트와 정반대의 trade-off). → 운영 가이드의 L4 관측 제약과 동일.
  • 신원이 access log에 안 찍힌다. TEST 3는 설정상 SPIFFE 검증이 켜졌음을 보지만, 게이트웨이가 실제로 본 client SPIFFE ID는 기본 로그에 없다. access log 포맷에 %DOWNSTREAM_PEER_URI_SAN%를 추가하면 직접 로깅된다.
  • PASSTHROUGH가 살아 있는지 함께 봐야 한다. outbound를 REGISTRY_ONLY로 전환하면 미등록 외부가 차단되며 본 mTLS 경로가 강제되는데, 이때 기존 httpbin control 경로의 거동도 같이 확인해야 회귀를 잡는다.

관련 파일 · 참조

manifest (ISTIO_MUTUAL 세트)

개념 · 비교

구조 정본HTTPS over mTLS 구조 — CRD 해부·장단점·활용·운영


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

검증 방법 요약: 공식 Istio/Envoy/curl 문서(레퍼런스·블로그·GitHub 이슈) 대조 + homelab 클러스터 실측(T08).

주장 판정 근거
C1. PASSTHROUGH는 TLS 미종단·SNI만 읽어 포워딩 → 메시 내부 leg는 평문 TCP+앱TLS, 게이트웨이는 호출자 신원을 암호학적으로 모른다 ✅ 문헌 확인 istio.io/…/tls-configuration
C2. Gateway.tls.mode ISTIO_MUTUAL은 mesh mTLS 종단 + requireClientCertificate로 SPIFFE client cert 강제 ✅ 문헌 확인 istio.io/…/gateway
C3. “종단하면 SNI 소비 → leg-2는 반드시 tcp로만 받아야 한다"는 절대 법칙 서술 ❌ 오류 — 본문 교정 istio.io/latest/blog/2023/egress-sni · T07 실측
C4. filter chain이 0개인 listener는 통째로 omit된다 ✅ 문헌 확인 github.com/istio/istio#17517
C5. DR portLevelSettings.tls.sni가 mesh mTLS SNI를 강제, Gateway.hosts와 불일치 시 연결 끊김 ✅ 문헌 확인 istio.io/…/destination-rule
C6. 같은 포트에 PASSTHROUGH+ISTIO_MUTUAL을 공존시키면 필연적으로 한쪽이 드롭된다 🔬 실측 반증 — 본문 교정 github.com/istio/istio#30978 · T08 실측
C7. DR trafficPolicy.tls.mode ISTIO_MUTUAL = source(sidecar)가 SPIFFE client cert로 TLS origination ✅ 문헌 확인 istio.io/…/destination-rule
C8. VS leg-1(gateways:[mesh]+tls/sniHosts) / leg-2(named egress Gateway route)의 2단 라우팅 구조 ✅ 문헌 확인 istio.io/…/egress-gateway
C9. curl ssl_verify_result=0 = 앱이 목적지 인증서를 직접 검증(게이트웨이가 안쪽 TLS를 풀지 않았다는 근거) ✅ 문헌 확인 curl.se/…/CURLINFO_SSL_VERIFYRESULT
C10. SPIFFE 식별 ≠ 인가 — AuthorizationPolicy(principals)를 별도로 걸어야 한다 ✅ 문헌 확인 istio.io/…/authorization-policy
C11. 클라이언트 SPIFFE ID는 기본 access log에 없고 %DOWNSTREAM_PEER_URI_SAN% 추가가 필요 ✅ 문헌 확인 envoyproxy.io/…/access_log/usage
C12. outboundTrafficPolicy 기본값 ALLOW_ANY, REGISTRY_ONLY 전환 시 미등록 호스트 차단 ✅ 문헌 확인 istio.io/…/egress-control
C13. apiVersion networking.istio.io/v1beta1이 “실제 manifest 기준"이라는 서술 ⚠️ 구버전 서술 — 갱신 istio.io/latest/blog/2024/v1-apis
C14. TLS Origination은 이 리포트(이중 TLS)와 정반대 트레이드오프의 다른 패턴 ✅ 문헌 확인 istio.io/…/egress-gateway-tls-origination
C15. Envoy cluster 이름 규칙은 direction|port|subset|fqdn 포맷 ✅ 문헌 확인 istio.io/…/proxy-cmd

Files