Test Report — Ingress / Egress Gateway 동작 검증
홈랩 클러스터에서 Ingress·Egress gateway 동작을 실측 검증한 리포트. 하나의 그림: gateway는 메시 경계에 선 전용 Envoy proxy다. ingress는 자기가 TLS를 끝내므로(termination) L7 전체가 보여 host/path로 분기하고, egress는 나가는 HTTPS를 복호화하지 않고 SNI만 보고 L4로 한 chokepoint를 거치게 강제한다 — 이 L7 vs L4 비대칭이 두 gateway 검증법까지 갈라놓는다. 결론 — Ingress: host/path 라우팅 + TLS termination PASS. Egress: HTTPS SNI PASSTHROUGH로 외부 호출이 egress gateway를 강제 경유함을 access log로 입증(200). REGISTRY_ONLY 미등록 차단은 메시 전역 영향 탓에 의도적 보류.
Date: 2026-06-07 Cluster: homelab (kubespray bare-metal, k8s v1.30.6, Calico) Istio: 1.30.0 (istiod + ingress/egress gateway, Helm 설치) Scenario: 10-ingress, 20-egress NS: mesh-test (istio-injection=enabled)
0. 배경 — 왜 gateway가 따로 있고, 왜 둘의 검증법이 다른가
sidecar가 이미 모든 pod에 붙어 mTLS·라우팅·관측을 한다면, 경계에 또 하나의 Envoy(gateway)를 세우는 이유는 무엇인가. 답은 경계 트래픽은 sidecar가 다루기 곤란한 특성을 갖기 때문이다.
- Ingress 쪽 문제: 외부에서 오는 클라이언트는 mesh identity(SPIFFE cert)가 없다. 그냥 공개 HTTPS로 들어온다. 누군가는 그 외부 TLS를 종료(termination)하고, 복호화한 Host/path를 보고 어느 내부 서비스로 보낼지 정해야 한다. 이걸 모든 pod에 흩뿌릴 수 없으니 단일 진입점이 필요하다.
- Egress 쪽 문제: 기본값(
outboundTrafficPolicy: ALLOW_ANY)에선 각 sidecar가 외부로 제멋대로 나간다. 그러면 egress IP가 노드마다 흩어지고, 방화벽 화이트리스트가 복잡해지고, 외부 호출 로그가 흩어진다. 그래서 나가는 트래픽을 한 pod(chokepoint)로 모아 단일 통제·관측 지점을 만든다.
그래서 ingress와 egress는 같은 "경계 gateway"지만 푸는 문제가 정반대다. ingress는 외부 TLS를 끝내고 안을 들여다봐야 하고(그래야 path로 분기), egress는 나가는 TLS를 그대로 둔 채 어디로 가는지만 알면 된다. 이 한 줄의 차이가 이 리포트의 핵심 — ingress = L7 termination, egress = L4 SNI passthrough — 으로 굳는다.
이 비대칭의 직접적 귀결: 검증법이 다르다. ingress는 "복호화가 됐나 + path가 맞게 갈렸나"를 status code(200/418/404)로 본다. egress는 payload가 암호화돼 gateway가 status를 볼 수 없으니, "200이 떴나"가 아니라 "정말 그 pod를 경유했나"를 access log로 본다. 이 차이를 모르면 egress의 200을 "gateway가 반환한 200"으로 오독한다(→ §2, What you might be missing).
선행 개념: Envoy listener/route/cluster, cluster 이름 규칙 direction|port|subset|fqdn, SNI(TLS handshake에 평문 노출되는 목적지 호스트명), ServiceEntry/Gateway/VirtualService/DestinationRule 4종 CRD. 깊은 레퍼런스는 Egress Gateway 정본, Cluster 해부.
1. 사전 확인 — 출발선이 깨끗한가
검증 전에 메시 자체가 정상이어야 결과를 믿을 수 있다.
- 메시 상태 정상: istiod/ingress/egress 모두
1/1 Running, proxy-status 2 proxies SYNCED. - ⚠️ 초기 "비정상" 의심은 istioctl 1.27 클라이언트로 1.30 컨트롤플레인을 조회한 버전 불일치 착시였음. 실제 이상 없음 — 진단 도구의 버전부터 의심하라는 교훈.
- sample app(
httpbin,sleep) READY 2/2 → sidecar 주입 정상. - baseline: 내부(sleep→httpbin) 200, 외부(httpbin.org) 200 (기본
outboundTrafficPolicy: ALLOW_ANY— 이 시점엔 egress gateway를 안 거치고도 외부가 뚫린다는 뜻. §2의 "경유 강제"는 이 baseline 위에 길을 새로 까는 작업이다).
2. Ingress Gateway — L7 termination 검증
메커니즘: ingress gateway는 외부에서 받은 HTTPS를 자기가 termination(복호화)한다. 일단 복호화하면 L7 전체(Host 헤더·path·method)가 보이므로, 그 정보로 내부 서비스에 분기할 수 있다. egress가 SNI만 보는 L4 라우팅인 것과 정확히 대비되는 지점이다 — 자기가 TLS를 끝내므로 L7 전체가 보인다. 그래서 검증 포인트는 둘로 갈린다: (1) TLS termination이 동작하는가, (2) 복호화된 L7로 host/path 분기가 올바른가(매칭 안 되면 404).
적용 manifest
scenarios/10-ingress/gateway-ingress.yaml— Gateway(selectoristio: ingressgateway), HTTP:80 + HTTPS:443(TLS termination,credentialName: httpbin-tls), hosthttpbin.example.comscenarios/10-ingress/virtualservice-httpbin.yaml—/status*명시 매칭 +/*catch-all →httpbin.mesh-test.svc:8000- TLS secret: 자체서명 cert →
kubectl -n istio-system create secret tls httpbin-tls(cert는tmp/certs/, gitignored)
검증 (NodePort http 31080 / https 31443, NODE=203.0.113.212)
각 테스트가 위 두 포인트 중 무엇을 입증하는지 보라 — 단순 200 나열이 아니다.
| 테스트 | 명령 | 기대 | 실제 | 입증 |
|---|---|---|---|---|
| HTTP catch-all | curl -H "Host: httpbin.example.com" http://NODE:31080/get |
200 | 200 | /* catch-all 도달 |
| path match | .../status/418 |
418 | 418 | /status* 명시 route가 catch-all보다 우선 |
| TLS termination | curl -k --resolve httpbin.example.com:31443:NODE https://.../get |
200 | 200 | gateway가 외부 TLS 복호화 |
| host 분기 | curl -H "Host: nope.example.com" .../get |
404 | 404 | 매칭 안 되는 host는 라우팅 거부 |
418이 200보다 중요하다: catch-all /*만 있었다면 /status/418도 200 본문을 받았을 것이다. 418이 떴다는 건 더 구체적인 /status* route가 먼저 매칭됐다는 뜻 — Envoy route 우선순위가 살아있음을 보여준다. 404(nope.example.com)는 그 반대편 증거: 정의되지 않은 host는 조용히 어디론가 가지 않고 거부된다.
istioctl proxy-config routes deploy/istio-ingressgateway.istio-system에http.8080/https.443.*route 반영 확인.istioctl analyze -n mesh-test→ 이슈 0.
합격 판정: PASS
외부→gateway 200, TLS termination 동작, host/path 라우팅 분기 정상.
3. Egress Gateway — L4 SNI passthrough + 경유 강제 검증
메커니즘과 "왜": 외부 대상이 HTTPS(httpbin.org:443)이면 sleep sidecar→egress 구간은 이미 암호화돼 있다 — gateway가 페이로드를 못 본다. 평문 HTTP였다면 gateway가 L7에서 헤더 보고 라우팅하겠지만, HTTPS면 그게 불가능하다. 그래서 헤더가 아니라 TLS handshake의 SNI(평문으로 노출되는 목적지 호스트명)로만 L4 라우팅한다. 이게 PASSTHROUGH 패턴이다 — gateway는 복호화 없이 SNI만 보고 흘려보낸다.
그러면 "굳이 왜 egw를 거치나?" §0의 답이 여기서 검증 질문을 정의한다: chokepoint를 만드는 게 목적이므로, 핵심 질문은 "호출이 성공하느냐"가 아니라 "호출이 정말 그 pod를 경유하느냐"다. 그래서 합격 판정도 단순 200이 아니라 200 + 경유 강제다. 개념·메커니즘 상세는 Egress Gateway 정본, passthrough vs TLS origination 비교는 Egress HTTP vs HTTPS 참조.
적용 manifest
scenarios/20-egress/serviceentry-httpbin-ext.yaml— httpbin.org:443 TLS,MESH_EXTERNAL,resolution: DNSscenarios/20-egress/gateway-egress.yaml— Gateway(selectoristio: egressgateway), 443 TLSPASSTHROUGHscenarios/20-egress/destinationrule-egress.yaml— egress gateway subsethttpbinscenarios/20-egress/virtualservice-egress.yaml— 2단 라우팅(tls:+sniHosts,http:아님 — SNI 기반 L4 라우팅): mesh leg(sleep→egress subset), egress leg(egress→httpbin.org)
이 3개 CRD의 값이 제각각이면 안 되고 한 줄로 정렬돼야 한다 — 셋 중 하나라도 L7(HTTP)을 가정하면 gateway가 복호화를 시도하다 깨진다.
SNI PASSTHROUGH 정합 3요소 — 셋이 모두 맞아야 L4 SNI 라우팅이 성립한다(상세: Egress Gateway 정본): - ServiceEntry 포트
protocol: TLS- Gateway servertls.mode: PASSTHROUGH- VirtualService를http:가 아닌tls:+sniHosts: [httpbin.org]로 작성
http:라우팅으로 작성하면 gateway가 TLS를 복호화하려다 실패한다.
검증 — config가 깔렸나 → 호출 됐나 → 경유했나
검증은 세 층으로 내려간다: ① Envoy에 config가 반영됐나, ② 실제 호출이 200인가, ③ 그 호출이 정말 egw를 통과했나(이게 핵심).
| 항목 | 확인 방법 | 결과 |
|---|---|---|
| sleep proxy에 egress cluster | proxy-config clusters deploy/sleep.mesh-test |
...egressgateway...443 httpbin subset cluster 존재 |
| egress gateway listener | proxy-config listeners deploy/istio-egressgateway |
0.0.0.0:8443 SNI: httpbin.org → outbound|443||httpbin.org ¹ |
| 실제 호출 | sleep -> curl -sI https://httpbin.org/get |
HTTP/2 200 |
| 경유 강제 | egress gateway access log 신규 라인 | outbound|443||httpbin.org 기록 (dest 44.213.156.185:443) |
¹ Gateway server는 443으로 정의했으나 실제 리스너는 8443이다. 비권한 포트로 listen하기 위해 Service 포트 443 → pod targetPort 8443으로 매핑한 결과이며, 포트 불일치가 아니다. Ingress의 http.8080(Service 80→targetPort 8080)과 동일한 패턴.
access log 발췌:
[2026-06-07T01:49:40Z] "- - -" 0 ... "44.213.156.185:443" outbound|443||httpbin.org
10.255.126.47:49456 10.255.126.47:8443 10.255.126.49:37006 httpbin.org -
gateway는 PASSTHROUGH이므로 이 로그는 HTTP 포맷이 아니라 TCP access log 포맷이다 — method/path/status code가 없고 SNI(httpbin.org)·바이트·duration·peer IP만 기록된다(L7 가시성 없음). 위 "- - -" 0을 위 표의 HTTP/2 200과 같은 라인으로 오해하지 말 것: 200은 egress가 본 status가 아니라 sleep→외부 end-to-end 호출 결과다. egress gateway pod IP 10.255.126.47:8443 수신 → 외부 44.213.156.185:443(httpbin.org) 송신 = 경유 확인. 로그에 이 라인이 새로 생겼다는 것 자체가 §1 baseline의 "egw 없이 직접 나가던" 경로가 egw를 통과하는 경로로 바뀌었다는 증거다.
합격 판정: PASS (경유 강제 + 200)
미수행 (의도적 보류)
- REGISTRY_ONLY 차단 테스트:
outboundTrafficPolicy는 mesh 전역(istio configmap) 설정 → 메시 전체 영향. 위험 작업 정책상 사용자 승인 후 별도 진행 예정. 현재는ALLOW_ANY유지 상태에서 VirtualService 기반 경유만 검증. (그래서 이 리포트는 경유 강제는 입증하지만 차단은 입증하지 못한다 — 둘은 다른 명제. → What you might be missing) - TLS origination(평문→egress에서 TLS 시작): 본 검증은 passthrough 채택. 비교는 Egress HTTP vs HTTPS, 별도 시나리오로 분리.
4. 트러블슈팅
kubectl apply -f scenarios/00-sample-apps/: 알파벳 순 처리로httpbin.yaml이namespace.yaml보다 먼저 적용되어 NotFound. 재적용(2회)으로 해결 — 디렉토리 apply 시 ns 선생성 의존성. (개선:--server-side또는 ns 분리 적용 권장)
5. 재현 명령 요약
# 0. sample apps
kubectl apply -f scenarios/00-sample-apps/ # ns 의존으로 2회 또는 ns 먼저
# 1. ingress
kubectl -n istio-system create secret tls httpbin-tls --cert=tmp/certs/cert.pem --key=tmp/certs/key.pem
kubectl apply -f scenarios/10-ingress/
NODE=203.0.113.212
curl -H "Host: httpbin.example.com" http://$NODE:31080/get
# 2. egress
kubectl apply -f scenarios/20-egress/
kubectl -n mesh-test exec deploy/sleep -c sleep -- curl -sI https://httpbin.org/get
kubectl -n istio-system logs deploy/istio-egressgateway | grep httpbin.org
6. 다음 작업
- REGISTRY_ONLY 전환 후 미등록 외부 차단 검증(승인 필요).
- 30-security: PeerAuthentication STRICT + AuthorizationPolicy.
- TLS origination egress 변형.
- ✅ ISTIO_MUTUAL egress(HTTPS over mTLS) 검증 완료 → Egress mTLS 리포트
핵심 정리
- 한 문장: ingress=L7 termination(복호화→host/path 분기), egress=L4 SNI passthrough(복호화 안 함→경유 강제). 이 비대칭이 검증법까지 가른다(ingress는 status code, egress는 access log).
- Ingress: host/path 라우팅 분기(404 포함,
/status*>/*우선순위) + TLS termination(credentialName) 모두 PASS. route는http.8080/https.443.*로 Envoy에 반영. - Egress(SNI PASSTHROUGH): ServiceEntry
protocol: TLS+ Gatewaytls.mode: PASSTHROUGH+ VirtualServicetls+sniHosts3요소가 한 줄로 정렬돼야 성립. http 라우팅 아님. - 경유 강제 입증: egress gateway access log에
outbound|443||httpbin.org(dest 44.213.156.185:443) 신규 라인 + 호출 200. PASSTHROUGH라 로그는 TCP 포맷(status 없음). - 포트 매핑: gateway listener 8443은 Service 443→targetPort 8443 비권한 매핑. 불일치 아님.
- 보류: REGISTRY_ONLY 미등록 차단은 메시 전역(
outboundTrafficPolicy) 영향 → 승인 후 별도 진행.
What you might be missing
- 200은 egress가 본 status가 아니다. PASSTHROUGH egress의 access log는 TCP 포맷이라 method/path/status code가 없다. 검증표의
HTTP/2 200은 sleep→외부 end-to-end 호출 결과이고, gateway가 본 것은 SNI·바이트·duration·peer IP뿐(L7 가시성 없음). 둘을 같은 라인으로 오해하면 "egress가 200을 반환했다"는 잘못된 결론에 이른다. 이건 비대칭의 직접적 귀결 — egress가 L4라서 status가 원래 안 보이는 것이다. - 경유 강제 ≠ 차단. 본 리포트는 VirtualService 기반 경유 강제만 입증했다. 미등록 외부 호출 차단은
outboundTrafficPolicy: REGISTRY_ONLY가 필요한데, 이 값은 mesh 전역(istio configmap) 설정이라 메시 전체에 영향을 준다.ALLOW_ANY상태에선 누군가 VirtualService를 우회해 여전히 직접 나갈 수 있으므로, 경유가 보장돼도 차단은 보장되지 않는다 — 별도 승인·검증이 필요하다. - ingress 404 vs egress 0: ingress의 미매칭은 L7이라 명확한
404로 떨어지지만, egress PASSTHROUGH에서 SNI 불일치는 L4라404가 아니라 connection reset/drop으로 나타난다. "왜 4xx가 안 보이지?"의 답은 같은 비대칭이다.
관련 파일 · 참조
Ingress - 📎 gateway-ingress.yaml · 📎 virtualservice-httpbin.yaml · 📎 10-ingress/README.md
Egress - 📎 gateway-egress.yaml · 📎 serviceentry-httpbin-ext.yaml · 📎 virtualservice-egress.yaml · 📎 destinationrule-egress.yaml · 📎 20-egress/README.md
검증 스크립트 · 설치 - 📎 traffic.sh · 📎 proxy-dump.sh · 📎 install/helm/README.md · 📎 verify.sh - 📎 원본 test-report · 설치 선행: Helm 재설치 런북 - ↗ Istio: Egress Gateways