homelab89 Docs Logs Legacy Files ☰ TOC 🌓
noteistio 2026-06-07istioingressegressgatewaysni

Test Report — Ingress / Egress Gateway 동작 검증

ABSTRACT

홈랩 클러스터에서 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 — 으로 굳는다.

flowchart LR
  client["external client<br/>(no mesh identity)"]
  igw["ingress GW<br/>TLS termination → L7"]
  svc["httpbin<br/>(internal)"]
  sleep["sleep sidecar<br/>(in mesh)"]
  egw["egress GW<br/>SNI passthrough → L4"]
  ext["httpbin.org:443<br/>(external HTTPS)"]
  client -- "HTTPS in" --> igw
  igw -- "decrypted Host/path route" --> svc
  sleep -- "HTTPS out (still encrypted)" --> egw
  egw -- "route by SNI only" --> ext

이 비대칭의 직접적 귀결: 검증법이 다르다. 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(selector istio: ingressgateway), HTTP:80 + HTTPS:443(TLS termination, credentialName: httpbin-tls), host httpbin.example.com
  • scenarios/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*/*보다 먼저 나열되어 매치(first-match-wins)
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는 라우팅 거부

418200보다 중요하다: catch-all /*만 있었다면 /status/418도 200 본문을 받았을 것이다. 418이 떴다는 건 /status* route가 매칭됐다는 뜻이지만, 이는 더 구체적인 경로가 자동으로 우선하는 것이 아니다 — Istio VirtualService의 HTTPRoute는 매니페스트 http: 리스트에 나열된 순서대로 첫 번째로 매치되는 규칙이 적용되는 first-match-wins 방식이며, /status*/*보다 먼저 나열돼 있었기 때문에 매칭된 것이다(순서를 바꿔 /*를 먼저 두면 /status/418도 catch-all에 잡혀 200이 반환된다 — T10 실측으로 확인, 검증 기록 참고). 404(nope.example.com)는 그 반대편 증거: 정의되지 않은 host는 조용히 어디론가 가지 않고 거부된다.

  • istioctl proxy-config routes deploy/istio-ingressgateway.istio-systemhttp.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: DNS
  • scenarios/20-egress/gateway-egress.yaml — Gateway(selector istio: egressgateway), 443 TLS PASSTHROUGH
  • scenarios/20-egress/destinationrule-egress.yaml — egress gateway subset httpbin
  • scenarios/20-egress/virtualservice-egress.yaml2단 라우팅(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 server tls.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
실제 호출 sleep -> curl -sI https://httpbin.org/get HTTP/2 200
경유 강제 egress gateway access log 신규 라인 `outbound

¹ 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를 통과하는 경로로 바뀌었다는 증거다.

flowchart LR
  sleep["sleep sidecar<br/>10.255.126.49"]
  egw["egress GW :8443<br/>PASSTHROUGH<br/>10.255.126.47"]
  ext["httpbin.org:443<br/>44.213.156.185"]
  sleep -- "mesh leg<br/>SNI route (tls/sniHosts)" --> egw
  egw -- "egress leg<br/>outbound|443||httpbin.org" --> ext

합격 판정: 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.yamlnamespace.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*/*보다 먼저 나열돼 매치되는 first-match-wins — 자동 specificity 아님) + TLS termination(credentialName) 모두 PASS. route는 http.8080/https.443.*로 Envoy에 반영.
  • Egress(SNI PASSTHROUGH): ServiceEntry protocol: TLS + Gateway tls.mode: PASSTHROUGH + VirtualService tls+sniHosts 3요소가 한 줄로 정렬돼야 성립. 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(TLS 계층 거부, curl exit=35/000)으로 나타난다. “왜 4xx가 안 보이지?“의 답은 같은 비대칭이다.

관련 파일 · 참조

Ingress

Egress

검증 스크립트 · 설치


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

검증 방법: 공식 Istio/Envoy 문서 대조 + homelab 클러스터(k8s 1.30.6, Istio 1.30.0) 실측 2건(T09, T10).

주장 판정 근거
C1. ingress gw는 외부 HTTPS를 termination하고 복호화된 Host/path로 L7 라우팅한다 ✅ 실측 확인 istio.io/…/secure-ingress · T09 실측
C2. egress PASSTHROUGH는 페이로드를 복호화하지 않고 SNI만으로 L4 라우팅한다 ✅ 문헌 확인 istio.io/…/egress-gateway (관련 재현 T47은 다른 문서[egress/crd-mental-model] 기준 hop-gw 트래픽 미수신으로 inconclusive — T47 실측)
C3. outboundTrafficPolicy 기본값은 ALLOW_ANY ✅ 문헌 확인 istio.io/…/istio.mesh.v1alpha1
C4. SNI는 TLS handshake 중 평문 노출되는 목적지 호스트명 ✅ 문헌 확인 envoyproxy.io/…/tls-sni
C5. SNI PASSTHROUGH는 http: 아닌 tls:+sniHosts로 작성해야 한다 ✅ 문헌 확인 istio.io/…/egress-gateway
C6. SNI PASSTHROUGH 3요소(SE TLS + Gateway PASSTHROUGH + VS tls/sniHosts) 정렬 필요 ✅ 문헌 확인 istio.io/…/egress-gateway
C7. PASSTHROUGH egress access log는 TCP 포맷(method/path/status 없음) ✅ 문헌 확인 istio.io/…/access-log
C8. Envoy cluster 이름 규칙은 direction|port|subset|fqdn ✅ 문헌 확인 istio.io/…/proxy-cmd
C9. /status/418이 418인 이유는 “더 구체적인 route가 자동 우선"이다 ❌ 오류 — 본문 교정 istio.io/…/virtual-service · T10 실측 (first-match-wins로 정정)
C10. Gateway listener 8443은 Service 443→targetPort 8443 매핑(포트 불일치 아님) ✅ 문헌 확인 istio.io/…/ist0162
C11. outboundTrafficPolicy는 mesh 전역(MeshConfig) 설정 ✅ 문헌 확인 istio.io/…/istio.mesh.v1alpha1 (namespace-scope Sidecar로 치환한 T02는 이 claim의 “전역 blast-radius” 자체를 검증하지 않음 — T02 실측)
C12. egress SNI 불일치는 404가 아니라 connection reset/drop(L4)으로 나타난다 ✅ 실측 확인 T09 실측 (curl exit=35, %{http_code}=000)
C13. 외부 클라이언트는 mesh identity가 없어 단일 진입점(ingress gw)이 필요하다 ✅ 문헌 확인 istio.io/…/traffic-management
C14. istioctl 1.27/1.30 버전 불일치가 진단 착시를 유발했다 실측 불가 istio.io/…/istioctl (버전 일치 권고는 문헌 확인되나, 이 세션의 구체적 착시 재현은 문서화돼 있지 않음)
C15. PASSTHROUGH 대안으로 TLS origination(egress gw가 TLS 개시) 패턴이 있다 ✅ 문헌 확인 istio.io/…/egress-gateway-tls-origination

Files