# Egress Gateway — ISTIO_MUTUAL (HTTPS over mTLS) 검증 리포트 **Date:** 2026-06-08 **호스트:** homelab (kubespray bare-metal, k8s v1.30.6, 3노드) **도메인:** Istio / Egress / Security **시나리오:** `scenarios/20-egress/` — `*-cnn-mtls` 세트 **상태:** ✅ PASS — sidecar↔egress 구간 메시 mTLS(SPIFFE 검증) + 앱 HTTPS end-to-end 유지 --- ## 1. 목적 기존 egress는 **HTTPS PASSTHROUGH**(httpbin.org) — 게이트웨이가 단일 TLS 레이어의 SNI만 보고 포워딩하며 메시 내부 leg는 평문 TCP(앱 TLS만 존재)라 **호출 주체를 암호학적으로 식별 못 함**. 본 검증은 그 보안 공백을 메우는 변형: **sidecar↔egress 구간을 Istio mTLS(`ISTIO_MUTUAL`)로 감싸고**, 앱은 그대로 HTTPS(end-to-end TLS)로 통신. 결과적으로 "**mTLS 터널 안으로 HTTPS가 흐르는**" 이중 TLS 구조. egress 게이트웨이가 호출 워크로드의 **SPIFFE 신원을 검증**할 수 있게 된다. 대상 외부: `edition.cnn.com`(별도 도메인 — 기존 httpbin passthrough를 control로 보존, 직접 비교). --- ## 2. 구조 (이중 TLS) ``` sleep(app) sidecar egress-gw(:15443) edition.cnn.com curl https:// ---> [outer: mesh mTLS / ISTIO_MUTUAL] --(terminate)--> :443 (app TLS 생성) \____ inner: app TLS (end-to-end) _________________/ (tcp_proxy) outer (sidecar<->egress) : Istio mTLS. egress-gw가 client cert 강제 + mesh CA 검증(SPIFFE). inner (app<->cnn) : 앱 HTTPS. egress-gw는 복호화 안 함 — 종단 후 tcp_proxy로 그대로 전달. ``` --- ## 3. manifest (4종, `scenarios/20-egress/`) | 파일 | 핵심 | PASSTHROUGH(httpbin) 대비 | |---|---|---| | `serviceentry-cnn-ext.yaml` | edition.cnn.com:443 TLS/DNS 등록 | 도메인만 다름 | | `gateway-egress-cnn-mtls.yaml` | egress Gateway server, **port 15443, `tls.mode: ISTIO_MUTUAL`** | PASSTHROUGH→ISTIO_MUTUAL, 포트 분리 | | `destinationrule-egress-cnn-mtls.yaml` | subset cnn, `portLevelSettings.tls: {mode: ISTIO_MUTUAL, sni: edition.cnn.com}` | **trafficPolicy 신규**(passthrough는 subset만) | | `virtualservice-egress-cnn-mtls.yaml` | leg-1 `tls`/sniHosts → egress gw:15443 / **leg-2 `tcp`** → cnn:443 | **leg-2가 tcp**(passthrough는 tls/sniHosts) | ### 설계상 두 개의 함정(실제로 밟음 → 교정) 1. **포트 충돌 — 한 포트에 PASSTHROUGH + ISTIO_MUTUAL 공존 불가.** 처음 443으로 만들었더니, 기존 httpbin PASSTHROUGH가 443을 점유 중이라 머지 단계에서 cnn 서버가 드롭(`filter_chain_not_found`). egress gw Service가 노출하는 별도 tls 포트 **15443**(→targetPort 15443)으로 분리해 해결. 2. **종단 listener에는 `tls`/sniHosts 라우트가 안 먹는다 → `tcp` 라우트 필수.** `ISTIO_MUTUAL`은 게이트웨이가 메시 mTLS를 **종단**한다. 종단 listener에 `tls`/sniHosts 2단 라우트를 걸면 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)**로 받아야 한다. 교정 후 listener 정상 생성: ``` 0.0.0.0 15443 SNI: edition.cnn.com Cluster: outbound|443||edition.cnn.com ``` --- ## 4. 적용 ```bash CTX=homelab cd scenarios/20-egress kubectl --context $CTX apply \ -f serviceentry-cnn-ext.yaml \ -f gateway-egress-cnn-mtls.yaml \ -f destinationrule-egress-cnn-mtls.yaml \ -f virtualservice-egress-cnn-mtls.yaml # dry-run=server / istioctl analyze : 사전 통과(이슈 0) ``` --- ## 5. 결과 / 검증 (기대 vs 실제) ### TEST 1 — 외부 호출 ```bash kubectl --context homelab -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 ``` ### 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 수신. - :15443 listener에서 수신 → `outbound|443||edition.cnn.com` 으로 tcp_proxy. ### TEST 3 — in-mesh leg가 mTLS인가 (egress-gw 15443 listener) ``` SNI match : ['edition.cnn.com'] requireClientCertificate : True ← client cert 강제(메시 mTLS) validationContext(SPIFFE) : True ← mesh CA로 client 신원 검증 server cert SDS : ['default'] ← 게이트웨이 자기 메시 신원 제시 ``` ### 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 제시 ``` ### 합격 판정 | 합격선 | 결과 | |---|---| | 외부 호출 200 | ✅ HTTP=200, ssl_verify=0 | | egress gateway 경유 | ✅ :15443 수신 → outbound cnn | | 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) | --- ## 6. 트러블슈팅 기록 | 증상 | 원인 | 해결 | |---|---|---| | `curl: (35) Connection reset` + egress-gw 로그 `NR filter_chain_not_found edition.cnn.com` | 443에 httpbin PASSTHROUGH가 선점 → cnn ISTIO_MUTUAL 서버 머지 시 드롭 | 포트를 15443으로 분리 | | 15443 listener 자체가 안 생김. istiod `omitting listener ... must have more than 0 chains` | 종단(ISTIO_MUTUAL) listener에 `tls`/sniHosts 라우트는 network filter 미생성 | VS leg-2를 `tcp` 라우트로 교정 | | httpbin control 간헐 503/timeout | 외부 httpbin.org flakiness × 기존 outlier detection DR(consecutive5xx) 증폭 | 본 변경과 무관(cnn 리소스가 httpbin 미참조) | --- ## 7. 학습 포인트 / 다음 작업 1. **PASSTHROUGH vs ISTIO_MUTUAL의 본질 차이는 "종단 여부"다.** PASSTHROUGH=단일 TLS, 게이트웨이가 안 푼다(SNI 라우팅, L4 관측만). ISTIO_MUTUAL=외부 메시 TLS를 종단·신원검증한 뒤 내부를 다시 흘린다. → 종단하면 SNI가 소비되므로 2단 라우팅을 `tls`(SNI)가 아니라 `tcp`로 받아야 한다. 2. **"HTTPS over mTLS"는 이중 TLS다.** 게이트웨이는 바깥 메시 mTLS만 풀고 안쪽 앱 TLS는 못 본다 → 관측은 여전히 L4(`istio_tcp_*`)뿐. (앱 HTTPS를 게이트웨이가 들여다보려면 TLS origination 패턴 필요 — 그건 앱이 평문 HTTP를 보내야 함.) 3. **한 egress gw에서 PASSTHROUGH와 TLS-terminate를 섞으려면 포트를 분리**해야 한다(같은 포트 머지 충돌). ### 다음 작업 - **SPIFFE 인가까지**: 지금은 신원 *식별*만. egress-gw에 `AuthorizationPolicy`(principals 기반)를 걸어 "특정 SA만 cnn으로 나갈 수 있다"는 *인가*를 추가 검증. - **REGISTRY_ONLY 전환** 후 미등록 외부 차단 + 본 mTLS 경로 강제 확인. - access log 포맷에 `%DOWNSTREAM_PEER_URI_SAN%` 추가해 게이트웨이가 실제로 본 SPIFFE ID를 로그로 직접 확인.