Egress "HTTPS over mTLS" (ISTIO_MUTUAL) — 구조·CRD·장단점·활용·운영
머릿속에 넣을 한 장의 그림: 이 패턴은 "end-to-end 암호화 보존"과 "egress에서 호출자 신원 식별"이라는
서로 독립인 두 축의 교집합 칸이다 — 둘 다 Yes일 때만 정답인 좁은 셀. 구현은 이중 TLS: 앱은 그대로
https://(inner end-to-end TLS)로 호출하고, sidecar↔egress 구간만 Istio 메시 mTLS(ISTIO_MUTUAL)로
한 겹 더 감싸(outer) gateway가 그 outer를 종단하며 호출 워크로드의 SPIFFE 신원을 암호학적으로 검증하되,
안쪽 앱 TLS는 풀지 않고 tcp_proxy로 외부까지 그대로 흘린다. passthrough(신원 없음)와 TLS origination(앱 평문화)의
사각지대를 메우는 패턴이며, 실측 검증은 Egress mTLS 리포트, 운영 정본은
Egress 운영, 개념 정본은 Egress Gateway 정본,
이 신원 위에 올라가는 통제(AuthorizationPolicy·테스트 매트릭스)는 Egress 신원 기반 통제 가이드 참조.
대상 환경: homelab (kubespray bare-metal, k8s v1.30.6, CNI Calico), Istio 1.30.0
검증 도메인: edition.cnn.com(외부 HTTPS) — 기존 httpbin.org passthrough를 control로 보존하고 직접 비교
전제 지식: sidecar/Gateway/VirtualService/DestinationRule 기본 + egress 2-leg 라우팅(정본 §04)
1. 배경 — 왜 이 패턴이 존재하나 (passthrough와 origination 사이의 빈칸)
egress gateway로 외부 HTTPS를 내보내는 방식은, 단 두 개의 독립된 질문으로 완전히 좌표가 잡힌다. 이 두 질문이 이 문서 전체의 출발점이다.
- gateway가 앱 payload를 복호화해도 되는가? (= end-to-end 암호화를 포기할 수 있나)
- egress에서 "누가 나가는지"를 식별해야 하는가? (= 호출 워크로드 신원이 필요한가)
표준 egress 패턴 두 개는 이 좌표의 대각선 두 칸만 채운다.
- PASSTHROUGH: 가장 단순·흔함. gateway는 SNI만 보고 앱 TLS를 그대로 흘린다 → end-to-end는 지켜지지만, in-mesh leg가 평문 TCP라 호출자가 누군지 암호학적으로 모른다. 상세 정본, 가이드 HTTPS passthrough 가이드.
- TLS origination: gateway가 외부로 TLS를 시작하므로 앱은 평문 HTTP를 메시에 보내야 한다 → gateway가 L7(method/path/status)을 보고 정책·감사·중앙 cert 관리가 가능하지만, end-to-end 암호화가 깨진다(payload가 gateway 메모리에 평문으로 존재). 비교 HTTP vs HTTPS.
여기서 빈 칸이 하나 남는다: "payload는 절대 못 보게 하면서(end-to-end) + 그래도 누가 나가는지는 알아야 한다." passthrough는 신원이 없어서, origination은 복호화를 해서 둘 다 이 칸을 못 채운다. 그 빈 칸을 메우려고 등장한 게 바로 이 "HTTPS over mTLS" 패턴이다. 이 한 문장이 이 패턴이 존재하는 유일한 이유이고, 이후 모든 설계 결정(이중 TLS, ISTIO_MUTUAL, leg-2가 tcp인 것)은 전부 이 칸을 채우기 위한 귀결이다.
end-to-end 암호화 보존?
Yes No
+----------------+----------------+
egress No | PASSTHROUGH | (의미 없음) |
신원 | 가장 흔함 | |
식별? +----------------+----------------+
Yes | HTTPS over mTLS | TLS origination|
| <-- 본 문서 | gw L7+cert중앙 |
+----------------+----------------+
세 패턴을 한 표로 정렬하면 "무엇을 누가 푸느냐"가 한눈에 보인다.
gateway가 앱 TLS를 in-mesh leg(sidecar->gw)가 gateway가 본
패턴 복호화하는가 암호+신원검증인가 L7(method/path/status)
--------------- ------------------- -------------------------- ---------------------
PASSTHROUGH No (SNI만 봄) No (평문 TCP, 앱 TLS만) No (L4 only)
HTTPS over mTLS No (이중 TLS) Yes (메시 mTLS 종단+SPIFFE) No (L4 only) <-- 본 문서
TLS origination Yes (gw가 종단) 선택(메시 mTLS는 별개) Yes (L7 visible)
여기서 이 패턴이 niche인 이유까지 미리 못 박아두자 — 뒤의 모든 설계 함정을 받아들이는 마음가짐이 된다. 본 패턴은 위 좌표의 한 칸일 뿐이고, 그 칸에 드는 시나리오 자체가 드물다. 대부분의 "단순 외부 HTTPS"는 passthrough로 충분하고(신원 불필요·cert 관리 없음·4객체 중 최단순), 신원·정책이 필요한 조직은 보통 origination을 택한다(egress gateway를 두는 주된 이유인 L7 감사/per-URL 정책/중앙 cert를 다 주므로). 본 패턴은 payload는 절대 노출 불가 + 신원은 필요라는 교집합에서만 최적이고, 결정타로 이 패턴을 써도 L7 egress 정책은 여전히 불가하다(이중 TLS라 gateway가 L7을 못 봄). 추가 복잡성으로 얻는 건 오직 "신원" 하나뿐이라, 많은 조직은 그 신원을 source IP / NetworkPolicy / namespace로 더 싸게 근사한다.
즉 드문 건 구조가 나빠서가 아니라, 그 효익(end-to-end + 신원)을 동시에 요구하는 시나리오가 드물어서다. 그 시나리오에 정확히 들면 이건 유일하게 맞는 패턴이다(구체 사례는 §5).
2. 아키텍처 — 이중 TLS의 데이터 경로 (앵커: "outer는 풀고, inner는 흘린다")
이 패턴 전체를 한 문장으로 압축하면 이렇다: gateway는 바깥 봉투(outer mesh mTLS)만 열어 "누가 보냈는지"를 확인하고, 안쪽 편지(inner 앱 TLS)는 봉인된 채로 외부까지 그대로 부친다. 이 한 그림에서 나머지가 전부 따라 나온다 — outer를 종단하니까 신원을 볼 수 있고, inner를 못 푸니까 L7도 못 보고, 종단된 listener에선 SNI가 이미 소비됐으니까 leg-2가 tcp여야 한다.
sleep(app) sleep sidecar egress-gw (:15443) edition.cnn.com:443
---------- ------------- ------------------ -------------------
curl https:// ──(A)──> [생성한 앱 TLS를 ───(terminate outer)───> [inner 앱 TLS를
(inner TLS 생성) outer mesh mTLS로 래핑] verify client SPIFFE 서버가 종단]
── outer: ISTIO_MUTUAL ──> re-emit inner via tcp_proxy
\________ inner: app TLS (end-to-end, gw가 못 봄) ________/
outer (sidecar<->gw) : Istio 메시 mTLS. gw가 client cert 강제 + mesh CA로 SPIFFE 검증 후 *종단*.
inner (app<->cnn) : 앱 HTTPS. gw는 복호화 안 함 — 종단 직후 tcp_proxy로 opaque bytes 그대로 전달.
봉투가 어떻게 열리고 닫히는지 단계별로 — 각 단계가 위 앵커의 어느 부분인지 의식하며 읽으면 된다.
- (A) 앱 — inner 봉인:
curl https://edition.cnn.com/. 앱이 직접 TLS handshake를 시작한다(inner TLS). 이 시점의 ciphertext는 끝까지 누구도 풀지 않는다 — 이게 "end-to-end 보존"의 물리적 실체다. - sidecar (leg-1, outer 봉투 생성): VirtualService leg-1이 이 트래픽을 egress gateway로 라우팅하고, DestinationRule이 그 leg를 메시 mTLS(
ISTIO_MUTUAL)로 래핑한다. sidecar는 자기 SPIFFE client cert를 제시하며 outer TLS를 만든다. outer의 SNI는 DR이 지정한edition.cnn.com— 이게 gateway가 filter chain을 고르는 키. - egress gateway (outer 봉투 개봉 + 신원 확정):
:15443listener가tls.mode: ISTIO_MUTUAL→ client cert를 강제(requireClientCertificate: true) 하고 mesh CA(validationContext)로 sidecar의 SPIFFE ID를 검증한 뒤 outer mTLS를 종단한다. 여기서 "누가 나가는가"가 암호학적으로 확정된다 — 이게 이 패턴이 passthrough 대비 유일하게 더 얻는 것. - egress gateway (leg-2, inner 봉인 그대로 발송): outer를 벗기고 나면 손에 남는 건 앱의 inner TLS ciphertext다. gateway는 이걸 풀 수 없고(앱↔cnn end-to-end), VirtualService leg-2의
tcp라우트(tcp_proxy) 가 이 opaque 바이트를outbound|443||edition.cnn.com으로 그대로 흘린다. - 외부 — inner 봉인 개봉:
edition.cnn.com:443가 inner TLS를 종단. 앱이 본ssl_verify=0은 앱↔cnn end-to-end 검증 결과 — gateway가 inner에 전혀 관여하지 않았음의 증거다.
왜 leg-2는
tls가 아니라tcp인가 (앵커에서 직접 따라 나오는, 이 패턴의 가장 큰 함정) outer를 종단하면 SNI는 그 자리에서 소비된다 — 더는 라우팅 키로 남아 있지 않다. 그런데 종단된 listener에tls/sniHosts라우트를 걸면 Envoy는 매칭할 SNI가 없어 network filter를 생성하지 못하고, listener가 통째로 누락된다(must have more than 0 chains로 omit). 종단 후 남은 inner 바이트를 흘리려면 SNI를 다시 보지 않는 L4tcp_proxy가 필요 → leg-2는 반드시tcp라우트. passthrough는 종단을 안 하므로 SNI가 끝까지 살아 있어 양쪽 leg 모두tls/sniHosts— 정반대다. (filter chain 생성 원리: Envoy filter chain)
2.1 CRD 4종 — 각 객체가 "봉투 메커니즘의 어느 부분"을 책임지나
scenarios/20-egress/*-cnn-mtls.yaml. 4개 객체는 위 데이터 경로의 서로 다른 부분을 맡는다. PASSTHROUGH 대비 무엇이 달라지는지(델타)만 좁혀 본다 — 차이가 곧 이 패턴의 본질이다.
| CRD | 답하는 질문 (봉투 경로의 역할) | passthrough 대비 델타 |
|---|---|---|
| ServiceEntry | "이 외부 호스트를 메시가 알아도 되나?" | 차이 없음 (외부 등록은 패턴 무관) |
| Gateway | "egress pod가 outer를 어떻게 받나?" | tls.mode: PASSTHROUGH → ISTIO_MUTUAL, 포트 443 → 15443 |
| DestinationRule | "sidecar가 outer를 어떻게 만드나?" | trafficPolicy.tls 신규(passthrough는 subset만) |
| VirtualService | "각 leg를 어떻게 라우팅하나?" | leg-2가 tls/sniHosts → tcp |
ServiceEntry — 외부 호스트 등록 (passthrough와 동일)
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata: { name: cnn-ext, namespace: mesh-test }
spec:
hosts: [edition.cnn.com]
ports:
- number: 443
name: tls
protocol: TLS # HTTPS 아님 — gateway가 L7을 파싱하지 않으므로 L4 TLS로 등록
resolution: DNS # istiod가 DNS로 실제 IP 해석
location: MESH_EXTERNAL
protocol: TLS인 이유가 곧 앵커의 따름정리다 — 이 패턴에서 gateway는 앱 TLS를 끝까지 안 푸므로 L7 HTTP가 아니라 L4 TLS. origination이었다면 여기가 HTTP(앱이 평문)였을 것이다. 외부 등록 자체는 패턴 무관이라 passthrough와 동일하다.
Gateway — egress pod에 ISTIO_MUTUAL server (★ outer 봉투를 여는 곳)
apiVersion: networking.istio.io/v1
kind: Gateway
metadata: { name: egress-cnn, namespace: mesh-test }
spec:
selector: { istio: egressgateway } # egress pod 라벨과 일치
servers:
- port:
number: 15443 # ★ 443 아님 — passthrough(443)와 머지 충돌 회피
name: tls-cnn
protocol: TLS
hosts: [edition.cnn.com]
tls:
mode: ISTIO_MUTUAL # ★ PASSTHROUGH가 아니라 메시 mTLS를 *종단*+신원검증
tls.mode: ISTIO_MUTUAL: 이 한 줄이 listener를 "client cert 강제 + mesh CA 검증 + 종단" 모드로 만든다. 결과 listener:requireClientCertificate: True,validationContext(SPIFFE): True, server cert SDSdefault(gateway 자기 메시 신원). = 데이터 경로 3단계의 그 listener.port.number: 15443: 같은 포트(443)에 PASSTHROUGH server와 ISTIO_MUTUAL server가 공존하면 머지 단계에서 한쪽이 드롭된다(→filter_chain_not_found). egress Service가 노출하는 표준 tls 포트 15443(→targetPort 15443)으로 분리해 해결. 종단 모드별로 포트를 갈라 쓰는 게 원칙.
DestinationRule — leg-1을 메시 mTLS로 래핑 (★ sidecar가 outer 봉투를 만드는 트리거)
apiVersion: networking.istio.io/v1
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 }
tls:
mode: ISTIO_MUTUAL # ★ sidecar가 client cert 제시 + outer mTLS 생성
sni: edition.cnn.com # ★ gateway가 filter chain 고르는 키
- passthrough DR은 subset만 정의하고
trafficPolicy.tls를 비운다(앱 TLS를 그대로 전달). 여기서는trafficPolicy가 신규 — 이게 leg-1(sidecar→gw)을 메시 mTLS로 감싸는 트리거다(데이터 경로 2단계). tls.mode: ISTIO_MUTUAL(DR 쪽): sidecar가 자기 SPIFFE client cert를 제시하도록 한다. Gateway의 ISTIO_MUTUAL(server)과 짝 → outer mTLS 성립.tls.sni: edition.cnn.com: outer TLS의 SNI를 명시. gateway server의hosts: [edition.cnn.com]/filter chain 매칭 키이자, 잘못 두면 SAN 불일치 SSL 오류의 원인. DR sni ↔ Gateway hosts ↔ VS sniHosts 3자 일치 필수(아래 정렬 지도).
VirtualService — 2-leg, leg별 라우트 타입이 다름 (★ tcp vs tls)
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata: { name: egress-cnn, namespace: mesh-test }
spec:
hosts: [edition.cnn.com]
gateways: [mesh, egress-cnn] # mesh=sidecar(leg-1), egress-cnn=gateway(leg-2)
tls: # leg-1: sidecar -> egress gw (종단 안 함 -> tls/sniHosts)
- match:
- gateways: [mesh]
port: 443
sniHosts: [edition.cnn.com]
route:
- destination:
host: istio-egressgateway.istio-system.svc.cluster.local
subset: cnn # DR subset cnn = 메시 mTLS 래핑
port: { number: 15443 }
tcp: # ★ leg-2: gw -> 외부 (gw가 종단함 -> tcp 필수)
- match:
- gateways: [egress-cnn]
port: 15443
route:
- destination:
host: edition.cnn.com
port: { number: 443 }
weight: 100
- leg-1 =
tls/sniHosts: sidecar는 outer를 만들지만 종단하지 않는다 → SNI가 살아 있으므로tls라우트. - leg-2 =
tcp: gateway가 outer를 종단했으므로 SNI는 이미 소비됨. inner 바이트를 흘리려면tcp_proxy→tcp라우트. (§2 함정 박스 참조) - gateway listener엔 호스트가 1개라
tcp포트 매칭만으로 충분. 한 포트에 여러 종단 호스트를 섞으려면 포트를 더 분리하거나 라우팅 설계를 다시 해야 한다.
정렬 지도 — 같은 magic string이 4객체에 흩어진다. 이 값들이 한 글자라도 어긋나면 filter chain 매칭이나 SAN 검증이 깨진다.
SNI host : DR tls.sni == Gateway hosts[] == VS sniHosts[] (모두 edition.cnn.com)
종단 포트 : DR port 15443 == Gateway port == VS leg-1 dest port == VS leg-2 match port
gateway ref: Gateway name egress-cnn == VS gateways[]에 등장
egress 대상: DR host(egressgateway FQDN) == VS leg-1 dest host
CRD 관계 한눈에
3. 예시와 결과 — 실측으로 봉투 메커니즘 입증
위 4객체를 mesh-test 네임스페이스에 apply한 뒤, 앵커의 세 주장("outer를 종단하며 신원을 본다 / inner는 풀지 않는다 / 그래서 200")을 각각 한 줄씩으로 검증한다. (전체 실측: Egress mTLS 리포트)
TEST 1 — end-to-end로 통하는가 + gateway가 inner를 안 건드렸나
kubectl exec -n mesh-test 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이 결정적이다 — 이건 앱↔cnn 사이의 cert 검증이 통과했다는 뜻이고, gateway가 inner에 끼어들었다면 SAN이 어긋나 0이 안 나온다. 즉 inner 봉인이 외부까지 온전했다는 증거다.
TEST 2 — gateway가 inner를 풀지 않고 tcp_proxy로 흘렸나 (egress-gw access log)
[11:14:34] 0 - - ... 2422 4731478 435 ... "151.101.3.5:443" outbound|443||edition.cnn.com
response flag가 -(성공, NR filter_chain_not_found 없음)이고, 4.7MB(4731478 bytes) 가 흘러갔다 — cnn 페이지 본문이 tcp_proxy로 opaque하게 통과했다는 뜻. cluster 이름 outbound|443||edition.cnn.com이 leg-2의 목적지(direction|port|subset|fqdn, subset 없음).
TEST 3 — in-mesh leg가 진짜 메시 mTLS인가 (egress-gw :15443 listener)
istioctl proxy-config listener deploy/istio-egressgateway.istio-system --port 15443 -o json \
| grep -E 'sni|requireClientCertificate|validationContext|secretName'
# requireClientCertificate : True ← client cert 강제(메시 mTLS)
# validationContext : True ← mesh CA로 SPIFFE 검증
requireClientCertificate: True + validationContext: True = "client cert를 강제하고 mesh CA로 검증한다" = 앵커의 '신원을 본다'가 설정 레벨에서 참. 이 두 필드가 곧 데이터 경로 3단계의 물증이다.
TEST 4 — sidecar leg-1이 outer mTLS를 래핑하고 sni를 넣었나 (sleep cluster)
istioctl proxy-config cluster deploy/sleep.mesh-test \
--fqdn istio-egressgateway.istio-system.svc.cluster.local -o json | grep -E 'sni|mode|subset'
# cluster outbound|15443|cnn|...egressgateway 에 tls(mode ISTIO_MUTUAL, sni edition.cnn.com)
cluster 이름 outbound|15443|cnn|...egressgateway의 subset cnn이 DR의 subset과 일치하고, 거기 tls 컨텍스트가 붙어 있다 = sidecar가 데이터 경로 2단계대로 outer를 만든다는 증거.
검증 요약선: HTTP=200 + ssl_verify=0(inner end-to-end) + requireClientCertificate=True(신원 강제) + validationContext=True(SPIFFE 검증) + access log response flag -(종단·전달 성공). 다섯 신호가 동시에 참이면 이중 TLS가 의도대로 동작한 것이다. 기존 httpbin passthrough(control)는 무손상(간헐 200, 503/timeout은 외부 flakiness + outlier DR).
실측은 처음 manifest 그대로는 실패했다 — 두 함정을 실제로 밟았다. ① 포트 443을 httpbin passthrough가 선점한 상태로 cnn ISTIO_MUTUAL server를 같은 443에 두니 머지 충돌로 cnn 서버가 드롭(
curl: Connection reset, egw logNR filter_chain_not_found) → 15443 분리로 FIX. ② leg-2에tls/sniHosts를 걸자 종단 listener에 network filter가 0개라 istiod가omitting listener ... must have more than 0 chains로 통째 omit →tcp로 FIX. (리포트 §3)
3.1 장점 / 단점 — 위 실측이 곧 근거
이 패턴을 채택했을 때 실제로 얻는 것과 치르는 비용. 각 항목의 "메커니즘적 근거"는 위 데이터 경로/실측에서 곧장 나온다.
장점
| 장점 | 메커니즘적 근거 |
|---|---|
| egress에서 호출자 SPIFFE 신원 식별 | gateway가 outer mTLS를 종단하며 client cert를 mesh CA로 검증(TEST 3). IP/namespace가 아닌 암호학적 워크로드 신원. |
| 신원 기반 egress 인가 가능 | 그 신원으로 AuthorizationPolicy(principals=cluster.local/ns/.../sa/...)를 gateway에 걸어 "특정 SA만 이 외부로" 강제. (authz 멘탈모델) |
| end-to-end 앱 암호화 보존 | gateway는 inner 앱 TLS를 복호화하지 않음(blind relay, TEST 1의 ssl_verify=0). payload가 gateway 메모리에 평문으로 없음 → PCI/PII 경계에서 gateway를 복호화 주체에서 제외. |
| mesh mTLS가 PERMISSIVE여도 leg-1 강제 암호화 | DR/Gateway의 명시적 ISTIO_MUTUAL이 mesh-wide 설정과 무관하게 in-mesh leg를 암호화+인증. |
| 외부 인증서 관리 불필요 | origination/MUTUAL-to-external과 달리 gateway가 외부용 cert를 보관·갱신할 필요 없음(앱이 알아서 TLS). |
단점
| 단점 | 메커니즘적 근거 |
|---|---|
| L7 사각은 그대로 | 이중 TLS라 gateway는 여전히 method/path/status를 못 봄(inner 미복호화) → per-URL egress 정책 불가, 관측은 istio_tcp_*(L4)뿐. egress gateway를 흔히 쓰는 이유(L7 감사/정책)를 못 얻는다. |
| 이중 TLS 비용 | handshake 2회(sidecar↔gw mesh mTLS + 앱↔외부) + 홉 1개 추가 → 지연·CPU 증가. |
| 설정 복잡성·함정 | 포트 머지 충돌(passthrough vs 종단), leg-2 tcp/tls 혼동, DR sni↔Gateway hosts↔VS sniHosts 3자 정렬. 실측에서 2개 함정을 실제로 밟음(§3 위). |
| Istio 단독으로는 강제 아님 | sidecar 우회(root/hostNetwork) 시 그냥 나감. 진짜 강제하려면 Calico NetworkPolicy로 egress pod 외 직접 송신 차단 필요. |
| 드물게 쓰임 → 자료·예제 적음 | 커뮤니티 레퍼런스가 passthrough/origination 대비 희소 → 트러블슈팅이 self-support. |
4. 활용 사례 — 이 패턴이 "유일하게 맞는" 경우 (조사)
§1에서 "교집합이 드물다"고 했다. 그 드문 교집합에 정확히 드는 구체 시나리오 — 여기선 다른 두 패턴이 둘 다 탈락한다. (웹 자료: Istio 공식·Tetrate·실무 가이드, §7)
-
규제·컴플라이언스 — "복호화 없는 감사" PCI-DSS/개인정보처럼 gateway가 payload를 복호화하면 안 되는 데이터를 외부 파트너로 보내면서도, "모든 외부 호출이 알려진 암호화 chokepoint를 지났고 + 어떤 워크로드(SA)가 호출했는지 감사 로그로 입증"해야 할 때. origination은 gateway가 복호화하므로 탈락, passthrough는 신원이 없어 탈락 → 이 패턴만 둘 다 만족.
-
워크로드 단위 egress allow-list (인가) "오직
sa/payment만partner-bank.example.com으로 나갈 수 있다"를 spoof 가능한 IP가 아니라 SPIFFE principal로 강제. gateway에AuthorizationPolicy(principals)를 걸어 구현. (authz, SPIFFE) -
멀티테넌트 클러스터에서 IP/namespace로는 신원이 불충분할 때 여러 팀이 한 클러스터를 공유하고 egress 권한을 팀(SA)별로 갈라야 하는데 namespace 라벨/IP가 신뢰 경계로 약할 때, mesh CA가 보증하는 SPIFFE가 더 강한 식별자.
-
외부가 HTTPS-only / cert-pinning이라 종단이 불가능한데도 신원이 필요할 때 외부 서버가 앱과의 end-to-end TLS(예: client cert pinning)를 요구해 gateway가 끼어들 수 없는데도 egress 신원 통제가 필요한 경계 사례.
거꾸로, L7 per-URL egress 정책이 목표라면 이 패턴은 틀린 선택이다(L7 사각). 그 경우 origination.
5. 운영 시 고려할 점 (의견)
전제: 채택 자체를 신중히 — "end-to-end 보존 + egress 신원"이 동시에 진짜 요구사항일 때만. 아니면 passthrough(단순) 또는 origination(L7).
- 신원만 식별하고 끝내지 말 것 = 인가까지 가야 의미. 종단+검증은 "누가 나가는지 식별"일 뿐. egress gateway에
AuthorizationPolicy(principals 기반)를 걸어 인가로 닫아야 비용이 정당화된다. 식별만 하면 비싼 로깅에 그친다. (리포트 §6 "다음 작업"이 정확히 이것) - access log에
%DOWNSTREAM_PEER_URI_SAN%추가. 그래야 gateway가 실제로 본 SPIFFE ID가 로그에 남아 감사 가치가 생긴다(기본 TCP access log엔 client SPIFFE ID가 안 찍힘). - Istio 라우팅 ≠ 강제. Calico로 닫아라. 본 homelab CNI는 Calico → 워크로드 pod의 egress를 egress gateway pod로만 허용하고 그 외 0.0.0.0/0 차단하는
NetworkPolicy/GlobalNetworkPolicy가 없으면 sidecar 우회로 새어 나간다. (정본 §02 강제 계층 — Cilium 언급은 Calico로 대치해 읽을 것) - 포트 위생. 종단 모드(ISTIO_MUTUAL) host-group마다 전용 포트(15443 등) 할당. 한 포트에 passthrough + 종단 절대 혼재 금지(머지 충돌). 종단 호스트가 늘면 포트/listener 설계를 먼저.
- L7 관측 공백은 앱 sidecar에서 메운다. gateway는 L4만 보므로, per-request egress 지표가 필요하면 호출 워크로드 sidecar의 L7 텔레메트리로 보완.
- HA. egress gateway는 SPOF. 본 검증 values는
replicaCount: 1→ 사내 적용 시 replica↑ + PDB + 노드 분산 필수. (운영 정본) - 신뢰 도메인 경계. outer leg 검증은 mesh CA(istiod) trust domain에 의존. 멀티클러스터/trust-domain federation이면
validationContext가 어느 CA를 신뢰하는지 재설계 필요. - 버전 정합. egress gateway ↔ istiod 버전 정렬(본 검증은 둘 다 1.30.0). 하위 버전 메시(예: 1.27.x)로 이식할 땐 istiod부터 정렬한 뒤 이 패턴을 적용.
핵심 정리
- 앵커 = 봉투 두 겹. outer(sidecar↔gw)는 메시 mTLS(
ISTIO_MUTUAL)로 gateway가 종단+SPIFFE 검증(누가 보냈나), inner(앱↔외부)는 앱 HTTPS를 gateway가 풀지 않고 end-to-end 보존(무엇을 보냈나는 끝까지 비밀). - leg-1
tls/ leg-2tcp규칙. sidecar는 outer를 만들기만 하므로 leg-1은tls/sniHosts(SNI 살아 있음). gateway는 outer를 종단해 SNI를 소비하므로 leg-2는tcp_proxy(=tcp라우트) 필수 —tls를 걸면 filter chain 0개로 listener가 통째 누락된다. - 포트 분리 15443. 같은 포트(443)에 PASSTHROUGH server와 ISTIO_MUTUAL server가 공존하면 머지 충돌로 한쪽 드롭 → 종단 모드는 전용 포트(15443)로 분리.
- 3자 SNI 정렬.
DR tls.sni==Gateway hosts==VS sniHosts가 일치해야 filter chain 매칭·SAN 검증이 성립. - 얻는 건 신원뿐, L7은 못 본다. 이중 TLS라 gateway는 method/path/status를 못 봐 per-URL 정책 불가·관측은
istio_tcp_*(L4)만 → 본질적으로 niche 패턴. - Istio 라우팅 ≠ 강제. sidecar 우회(root/hostNetwork)로 새므로 진짜 강제는 Calico
NetworkPolicy로 egress pod 외 직접 송신을 차단해야 완성된다.
6. What you might be missing
- 이 패턴의 "신원"은 in-mesh leg 한정이다. gateway가 검증하는 SPIFFE는 sidecar(호출 워크로드)의 신원이지, 외부 서버의 신원이 아니다. 외부 서버 인증은 여전히 앱의 inner TLS가 책임진다(앱이
ssl_verify). 외부 서버를 gateway가 검증하게 하려면 origination(gateway가 외부와 MUTUAL/SIMPLE)으로 가야 하고, 그건 end-to-end를 포기하는 것 — 둘을 동시에는 못 가진다. ISTIO_MUTUAL(DR/Gateway)과PeerAuthentication STRICT는 다른 레이어다. 전자는 이 egress leg의 전송 보안을 명시 지정, 후자는 수신측 워크로드가 평문을 거부하는 정책. egress mTLS를 켰다고 메시 전체 STRICT가 되는 게 아니다. (보안 3리소스)- 이중 TLS는 관측 도구를 헷갈리게 한다. Kiali/Prometheus에서 이 트래픽은
istio_tcp_*로만 잡히고 L7 그래프엔 안 뜬다 — "트래픽이 안 보인다"가 아니라 L4로 보고 있는 것. passthrough와 동일한 가시성 한계. tcp라우트의 호스트 다중화 한계. leg-2가 포트 매칭만으로 동작하는 건 그 listener에 호스트가 1개여서다. 한 종단 포트로 여러 외부 호스트를 라우팅하려면 SNI가 이미 소비된 종단 listener에선 까다롭다 — 호스트별 포트 분리가 현실적.resolution: DNS의 노드 DNS 의존. 외부 해석은 istiod/노드 DNS에 달려 있어 DNS 장애가 egress 장애로 직결. (DNS resolution 리포트)
7. 참조
아카이브 내부 - Egress mTLS 리포트 — 실측 · Egress Gateway 개념 정본 · Egress 운영 정본 - HTTP vs HTTPS egress · HTTPS passthrough 가이드 - SPIFFE 신원 · AuthorizationPolicy 멘탈모델 · 보안 3리소스 - Envoy filter chain · sidecar scope
관련 IaC (실제 manifest) - 📎 gateway-egress-cnn-mtls.yaml · 📎 destinationrule-egress-cnn-mtls.yaml - 📎 virtualservice-egress-cnn-mtls.yaml · 📎 serviceentry-cnn-ext.yaml · 📎 20-egress/README.md
외부 (조사 출처) - ↗ Istio: Understanding TLS Configuration - ↗ Istio: Egress Gateways with TLS Origination - ↗ Tetrate: Enforce egress traffic using Istio's authorization policies - ↗ Istio: Security Best Practices
반대 선택의 근거 → 이중 TLS 없이 egress 신원 — passthrough + Calico