🏠 목록 Istio 1.30 Egress Gateway — 외부 HTTPS 통신 설치·구성·테스트 가이드 📄 MD 원본 🌓 테마
istioegressgatewaytls-passthroughsni

Istio 1.30 Egress Gateway — 외부 HTTPS 통신 설치·구성·테스트 가이드

NOTE

homelab(kubespray bare-metal, k8s v1.30.6, CNI Calico, Istio 1.30.0)에서 egress gateway를 Helm으로 구성하고, 앱이 직접 https://를 호출하는 TLS Passthrough(SNI 라우팅) 시나리오를 끝까지 구성·검증한다. 머릿속에 담을 한 장의 그림: 메시의 모든 외부 송신을 egress gateway라는 단일 choke point로 모으되, TLS는 끝까지 암호화된 채로 두고 gateway는 SNI만 보고 라우팅한다(2-홉: mesh→gateway, gateway→external). 핵심 결론: egress의 "완료"는 200이 아니라 트래픽이 egress gateway를 실제로 경유했음을 증명하는 것이며, 호출 결과 / proxy-config / access log 세 가지를 교차 확인한다.

대상 환경: homelab (kubespray bare-metal, k8s v1.30.6, CNI Calico, 3노드), Istio 1.30.0 (Helm chart) 범위: egress gateway Helm 설치/구성 → 외부 HTTPS 테스트 앱 구성 → 필요한 Istio 객체 → 테스트·검증 절차 난이도 전제: Istio sidecar/Gateway/VirtualService 기본 개념을 알고 있음. egress 특유의 동작에 초점.


0. 배경 지식 — 왜 egress gateway를 거치게 만드는가

기본 상태의 메시는 외부 송신을 막지 않는다. sidecar의 outboundTrafficPolicyALLOW_ANY이면 각 워크로드의 Envoy가 모르는 목적지를 PassthroughCluster로 그냥 흘려보낸다 — pod마다 인터넷으로 나가는 구멍이 하나씩 생기는 셈이다. 이게 운영에서 곤란한 이유는 세 가지다:

egress gateway는 이 흩어진 송신을 단일 지점(choke point) 으로 모으는 전용 Envoy다. 메시의 외부 송신이 모두 이 pod 한 종류를 통과하면 — 출발 IP가 하나로 고정(방화벽 화이트리스트 가능), 송신 로그가 한 곳에 모이고 (감사 가능), 정책을 이 한 지점에 걸 수 있다(강제 지점 확보). 이 문서는 그 choke point를 homelab에 실제로 세우고, 앱이 https://를 직접 호출하는 가장 흔한 케이스를 통과시킨 뒤, 트래픽이 정말 그 지점을 경유했는지까지 증명한다.

선행 개념 한 줄씩 (모르면 먼저 채울 것):

개념 이 문서에서 왜 필요한가
sidecar 트래픽 캡처(:15001 outbound) 앱이 보낸 패킷을 Envoy가 가로채야 egress로 우회시킬 수 있다
outboundTrafficPolicy (ALLOW_ANY / REGISTRY_ONLY) 통제를 "강제"로 바꾸는 스위치 — §7.4의 차단 검증 핵심
TLS handshake의 SNI passthrough에서 gateway가 라우팅에 쓸 수 있는 유일한 평문 키
Gateway / VirtualService / DestinationRule / ServiceEntry 2-홉을 잇는 4객체 (§5에서 관계, §6에서 YAML)

왜/모드결정/2-leg 라우팅의 개념 정본egress gateway 개념 정본 §01·§02·§04에 있다. 본 가이드는 그 개념을 homelab에서 실제로 구성·검증하는 절차에 집중하므로 이론은 정본에 위임하고 여기서는 "왜 이 객체가 이 모양인지"만 짚는다.


1. 핵심 아키텍처 — 한 장의 그림과 그로부터 따라오는 모든 것

머릿속 앵커 한 문장: choke point로 모으되 TLS는 절대 풀지 않는다. 이 한 가지 제약이 이후 모든 설계를 결정한다.

namespace: mesh-test (injection ON) sleep (curl) (0) 15001 outbound capture · SNI만 readable sleep sidecar (Envoy) :15001 outbound capture hop1: mesh → egress (SNI) end-to-end TLS (ciphertext kept) namespace: istio-system istio-egressgateway (Envoy, ClusterIP) single egress choke point hop2: egress → external (PASSTHROUGH) end-to-end TLS (ciphertext kept) httpbin.org:443 (external)
그림 1. 2-홉 egress 경로 — sidecar가 :15001에서 outbound를 가로채지만 SNI만 읽고 payload는 암호화 유지. hop1(mesh→egress)·hop2(egress→external) 모두 PASSTHROUGH라 ciphertext가 외부 호스트까지 그대로 전달된다.

그림이 말하는 핵심은 2-홉이다. 외부 호출이 sidecar에서 외부로 직접 가지 않고, 일부러 한 번 더 꺾여 egress gateway를 경유한다(hop1: mesh→gateway, hop2: gateway→external). 이 "일부러 꺾기"가 choke point를 만든다. 그리고 양쪽 홉 모두 암호문이 그대로 유지된다(end-to-end TLS) — gateway는 봉투를 뜯지 않는다.

여기서 핵심 긴장이 나온다. choke point로 모으면 보통은 "거기서 트래픽을 들여다보겠다"가 따라오는데, 앱이 이미 https://종단간 암호화를 걸어 보냈으므로 gateway가 봉투를 뜯으면 그 암호화가 깨진다. 그래서 봉투를 안 뜯는다 — 이게 TLS Passthrough다. 봉투를 안 뜯으니 gateway가 라우팅에 쓸 수 있는 정보는 평문 HTTP 헤더/경로가 아니라, TLS handshake 때 평문으로 노출되는 목적지 호스트명 — SNI 하나뿐이다.

이 SNI 제약이 §6의 모든 객체 모양을 한 줄로 설명한다. 왜 이 모양인가를 미리 깔아두면 §6 YAML이 전부 "당연"해진다:

설계 선택 SNI 제약에서 따라오는 이유
ServiceEntry protocol: TLS (HTTP 아님) Envoy가 평문 헤더를 못 보니 L7 HTTP로 등록할 수 없다 → L4 TLS
Gateway server tls.mode: PASSTHROUGH 봉투를 뜯지 않고 그대로 통과 → 종단간 암호화 유지
VirtualService tls: 라우팅 (http: 아님) 라우팅 키가 경로/헤더가 아니라 sniHosts
access log가 L4 포맷 (status/path 없음) gateway가 L7을 못 보니 SNI·bytes·duration만 기록
미등록 차단 신호가 000(L4 reset) passthrough는 L7 응답을 만들 수 없어 연결 자체를 끊음

TLS Passthrough(SNI 기반)는 가장 흔한 "외부 HTTPS" 케이스이며 인증서 관리가 필요 없다(봉투를 안 뜯으니 gateway가 인증서를 가질 이유가 없다). TLS를 일부러 풀어 L7 가시성을 얻는 반대 선택지(TLS origination)는 §8에서 다룬다 — 거기서는 이 표의 모든 "이유"가 정반대로 뒤집힌다.


2. 사전 조건 (현재 상태 확인)

이 가이드는 Istio 1.30.0이 Helm으로 이미 설치되어 있고 egress gateway deployment가 떠 있는 상태를 전제한다(repo docs/runbooks/2026-06-01_istio-1.30-helm-reinstall.md에서 완료됨).

⚠️ 버전 skew 주의: 이 환경 istiod는 1.30.0이지만 로컬 istioctl 클라이언트는 1.27.0이다. proxy-status/proxy-config가 버전 불일치 경고나 일부 필드 누락을 보일 수 있으나 client 표시 한계일 뿐 메시 동작 이상이 아니다(상세: ingress·egress 리포트 §0).

먼저 확인:

CTX=homelab; NS=istio-system

# control plane + gateway가 모두 deployed / Running 인지
helm --kube-context $CTX -n $NS list
#  istio-base / istiod / istio-ingressgateway / istio-egressgateway  모두 1.30.0 deployed

kubectl --context $CTX -n $NS get deploy istio-egressgateway
#  istio-egressgateway   1/1

kubectl --context $CTX -n $NS get svc istio-egressgateway
#  ClusterIP  포트 15021,80,443,15443

만약 egress gateway가 없다면 §3부터, 이미 있다면 §3은 "구성 확인"용으로 읽고 §4로 진행한다.


3. Egress Gateway Helm 설치·구성

3.1 chart 구조 — gateway 차트 하나, values만 다름

Istio는 ingress/egress를 동일한 istio/gateway 차트로 만든다. 차이는 values뿐이다. egress의 핵심 선택:

3.2 values 파일 (values-egress-gateway.yaml)

repo 경로: install/helm/values-egress-gateway.yaml — 이미 존재. 외부 참조용으로 전문 수록.

# egress gateway — chart: istio/gateway 1.30.0
# 메시 -> 외부 송신을 단일 지점으로 모아 통제. ClusterIP(외부 노출 불필요).
name: istio-egressgateway

labels:
  app: istio-egressgateway
  istio: egressgateway        # <-- Gateway 리소스의 selector(istio: egressgateway)와 반드시 일치

service:
  type: ClusterIP             # egress는 외부 노출 안 함. 메시 내부 트래픽만 경유.
  ports:
    - name: status-port
      port: 15021
      targetPort: 15021
    - name: http2
      port: 80
      targetPort: 8080
    - name: https
      port: 443
      targetPort: 8443
    - name: tls
      port: 15443
      targetPort: 15443

autoscaling:
  enabled: false
replicaCount: 1

resources:
  requests:
    cpu: 50m
    memory: 128Mi

가장 중요한 한 줄: labels.istio: egressgateway. 이후 만들 Gateway 리소스가 selector: { istio: egressgateway }로 이 pod들을 찾는다. 라벨이 어긋나면 Gateway가 어떤 Envoy도 프로그래밍하지 못하고 트래픽이 흐르지 않는다(에러도 안 나서 디버깅이 까다롭다).

3.3 설치 / 갱신

CTX=homelab; NS=istio-system; VER=1.30.0

# (최초 1회) repo 등록
helm --kube-context $CTX repo add istio https://istio-release.storage.googleapis.com/charts
helm --kube-context $CTX repo update

# egress gateway 설치/갱신 (idempotent)
helm --kube-context $CTX upgrade --install istio-egressgateway istio/gateway \
  -n $NS --version $VER -f install/helm/values-egress-gateway.yaml --wait --timeout 3m

repo 루트라면 make install-gateways가 ingress/egress를 함께 처리한다.

3.4 설치 검증

# pod 1/1, deployment 정상
kubectl --context $CTX -n $NS get deploy,pod -l istio=egressgateway

# Envoy가 istiod와 xDS sync 됐는지 (CDS/LDS/EDS/RDS SYNCED)
istioctl --context $CTX proxy-status | grep egressgateway
#  istio-egressgateway-xxxx.istio-system   ...   SYNCED   SYNCED   SYNCED   SYNCED   istiod-...
#  (열 순서 = CDS LDS EDS RDS — 4개 모두 SYNCED 여야 합격)

# 경고 0
istioctl --context $CTX analyze -A

합격선: istio-egressgateway 1/1 Running, proxy-status에서 CDS/LDS/EDS/RDS 모두 SYNCED, analyze 경고 0.

이 시점에서 egress gateway는 떠 있지만 아무 트래픽도 받지 않는다. Gateway/VirtualService를 붙이기 전까지는 빈 Envoy다. 다음 단계부터 트래픽을 흘린다.


4. 테스트 앱 구성 (트래픽 소스)

외부 HTTPS를 호출할 클라이언트가 필요하다. repo의 mesh-test 네임스페이스 + sleep(curl 컨테이너)을 쓴다. egress는 "나가는" 트래픽 검증이므로 서버(httpbin) 없이 클라이언트만 있으면 된다.

4.1 네임스페이스 + sleep 배포

# namespace.yaml — sidecar 자동 주입 활성화 (이게 있어야 egress 통제가 가능)
apiVersion: v1
kind: Namespace
metadata:
  name: mesh-test
  labels:
    istio-injection: enabled
# sleep.yaml — 트래픽 소스(클라이언트). curl 이미지로 외부 호출.
apiVersion: v1
kind: ServiceAccount
metadata:
  name: sleep
  namespace: mesh-test
---
apiVersion: v1
kind: Service
metadata:
  name: sleep
  namespace: mesh-test
  labels: { app: sleep, service: sleep }
spec:
  ports:
    - name: http
      port: 80
  selector: { app: sleep }
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sleep
  namespace: mesh-test
spec:
  replicas: 1
  selector:
    matchLabels: { app: sleep }
  template:
    metadata:
      labels: { app: sleep }
    spec:
      serviceAccountName: sleep
      containers:
        - name: sleep
          image: curlimages/curl
          command: ["/bin/sleep", "infinity"]
          imagePullPolicy: IfNotPresent
          resources:
            requests: { cpu: 10m, memory: 32Mi }
kubectl --context homelab apply -f namespace.yaml -f sleep.yaml
# repo라면: make apps  (또는 kubectl apply -f scenarios/00-sample-apps/)

4.2 주입 확인 (가장 흔한 함정)

kubectl --context homelab -n mesh-test get pod
#  sleep-xxxx   2/2   Running     <-- 반드시 2/2 (app + istio-proxy)

READY가 1/2가 아니라 2/2여야 한다. 1/1이면 sidecar가 안 붙은 것 → egress gateway 통제 자체가 불가능(sidecar가 트래픽을 가로채지 못함). 네임스페이스 라벨 istio-injection=enabled을 확인하고 pod를 재생성(kubectl rollout restart deploy/sleep -n mesh-test).

4.3 baseline 호출 (현재는 sidecar가 직접 나감)

아직 egress 객체가 없으므로, 기본 ALLOW_ANY 상태에서는 외부 호출이 그냥 된다(egress gateway 경유 X). 이게 baseline이다.

kubectl --context homelab -n mesh-test exec deploy/sleep -c sleep -- \
  curl -sS -o /dev/null -w "%{http_code}\n" https://httpbin.org/get
#  200   <-- 단, 지금은 egress gateway를 안 거치고 sidecar가 직접 송신

→ 목표는 이 트래픽을 egress gateway로 강제 경유시키고, 나아가 미등록 외부를 차단하는 것.


5. 외부 HTTPS를 위한 Istio 객체 — 개념 맵

TLS Passthrough(SNI) 시나리오에 필요한 객체는 4개. §1에서 "각 객체가 왜 이 모양인지"는 SNI 제약으로 이미 깔았다. 여기서는 4객체가 2-홉을 어떻게 잇는지(관계)만 본다. 각 객체의 역할 한 줄 설명은 중복을 피해 §6의 YAML 주석으로 일원화한다. 부품을 "그게 답하는 질문"으로 읽으면 직관적이다:

객체 답하는 질문
ServiceEntry 이 외부 호스트가 메시 레지스트리에 존재하는가(화이트리스트)
Gateway (egress) egress pod의 어느 포트에 어떤 server(여기선 443 PASSTHROUGH)를 여는가
DestinationRule hop1의 목적지(egress 서비스)를 어떤 subset으로 부를 것인가
VirtualService hop1·hop2를 어디로 라우팅하는가 (sniHosts 매칭)
VirtualService 2-hop tls routing ServiceEntry httpbin-ext DestinationRule egressgateway subset Gateway (egress) istio=egressgateway httpbin.org:443 (ServiceEntry host) registry (whitelist) hop1 dest subset hop1: mesh to egress (SNI) hop2: egress to httpbin.org:443
그림 2. TLS Passthrough 4객체 관계 — VS가 중심, SE(registry whitelist)·DR(subset)을 참조하고 hop1은 Gateway, hop2는 외부 호스트로 라우팅.

추가(차단 검증용): - outboundTrafficPolicy: REGISTRY_ONLY (mesh 전역 또는 Sidecar 리소스) — ServiceEntry 없는 외부를 막아 "egress 통제가 실제로 강제되는가"를 증명한다.


6. Istio 객체 설정 (TLS Passthrough / SNI) — 전체 YAML

아래 5개 파일은 repo scenarios/20-egress/에 두는 것을 권장(파일명은 repo 컨벤션 kind-목적.yaml). 외부 참조용으로 전문 수록. 등록 외부 호스트는 프로젝트 컨벤션대로 httpbin.org 사용.

네임스페이스 배치 주의: 이 가이드는 시나리오 격리를 위해 4종 객체(ServiceEntry/Gateway/DestinationRule/ VirtualService)를 트래픽 소스와 같은 mesh-test에 둔다. Gateway selector는 네임스페이스를 가로질러 istio-system의 egress pod(라벨 istio=egressgateway)를 찾으므로 동작한다(객체 ns ≠ pod ns여도 무방). 정본 egress gateway 개념 정본 §04는 운영 표준으로 이 4종을 전부 istio-system에 집중 배치하길 권장한다 — 둘 다 동작하나, 운영 일관성·RBAC 경계 면에서 정본 쪽이 기준이다.

이름 주의 (인라인 vs 첨부): 아래 인라인 YAML의 리소스 이름은 본문 설명용이며 내부적으로 일관된다 (Gateway/VS istio-egressgateway·direct-httpbin-through-egress, DR egressgateway-for-httpbin, §7 검증·§10 cleanup과 일치). 문서 말미 「관련 파일」의 📎 첨부 파일은 repo 컨벤션상 다른 이름(egress-httpbin / egressgateway-httpbin)을 쓴다. 둘을 섞지 말고 적용한 쪽 이름으로 §7 검증·§10 cleanup을 맞출 것 — 첨부를 그대로 apply했다면 cleanup의 리소스 이름도 첨부 이름으로 바꿔야 한다.

6.1 ServiceEntry — 외부 호스트 등록

# serviceentry-httpbin-ext.yaml
# 외부 도메인을 메시 레지스트리에 등록. 이게 있어야 REGISTRY_ONLY에서도 통과(화이트리스트).
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: httpbin-ext
  namespace: mesh-test
spec:
  hosts:
    - httpbin.org
  ports:
    - number: 443
      name: tls
      protocol: TLS        # 앱이 직접 TLS -> protocol은 TLS (HTTPS가 아니라 TLS)
  resolution: DNS          # istiod가 DNS로 실제 IP 해석
  location: MESH_EXTERNAL  # 메시 밖 목적지

포인트: passthrough에서는 Envoy가 평문 HTTP 헤더를 못 본다(이미 암호화됨). 그래서 L7 HTTP가 아니라 L4 TLS 프로토콜로 등록하고, 라우팅 키는 TLS handshake의 SNI가 된다.

6.2 Gateway — egress gateway에 TLS PASSTHROUGH server

# gateway-egress.yaml
# egress gateway pod(selector istio=egressgateway)의 443 포트에 PASSTHROUGH server를 연다.
apiVersion: networking.istio.io/v1
kind: Gateway
metadata:
  name: istio-egressgateway
  namespace: mesh-test
spec:
  selector:
    istio: egressgateway     # <-- values의 labels.istio 와 일치해야 함
  servers:
    - port:
        number: 443
        name: tls
        protocol: TLS
      hosts:
        - httpbin.org
      tls:
        mode: PASSTHROUGH    # TLS를 풀지 않고 그대로 통과(종단간 암호화 유지)

6.3 DestinationRule — egress gateway subset

# destinationrule-egress.yaml
# hop 1의 목적지(egress gateway 서비스)에 대한 subset 정의. passthrough라 TLS 설정은 비움.
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: egressgateway-for-httpbin
  namespace: mesh-test
spec:
  host: istio-egressgateway.istio-system.svc.cluster.local
  subsets:
    - name: httpbin
      # PASSTHROUGH 모드에서는 여기서 TLS를 다시 만지지 않는다(앱 TLS를 그대로 전달).

6.4 VirtualService — 2-홉 SNI 라우팅 (핵심)

# virtualservice-egress.yaml
# hop 1(mesh -> egress gateway) + hop 2(egress gateway -> external) 를 한 파일에.
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: direct-httpbin-through-egress
  namespace: mesh-test
spec:
  hosts:
    - httpbin.org
  gateways:
    - mesh                  # sidecar (= 메시 내부 모든 워크로드)
    - istio-egressgateway   # 위 Gateway 리소스 이름
  tls:
    # --- hop 1: sleep sidecar -> egress gateway ---
    - match:
        - gateways: [mesh]
          port: 443
          sniHosts: [httpbin.org]
      route:
        - destination:
            host: istio-egressgateway.istio-system.svc.cluster.local
            subset: httpbin
            port:
              number: 443
    # --- hop 2: egress gateway -> 실제 외부 ---
    - match:
        - gateways: [istio-egressgateway]
          port: 443
          sniHosts: [httpbin.org]
      route:
        - destination:
            host: httpbin.org   # 진짜 외부 (ServiceEntry로 등록됨)
            port:
              number: 443
          weight: 100

tls 라우팅을 쓰는 이유: passthrough에서 Envoy가 볼 수 있는 건 TLS handshake의 SNI뿐이다. HTTP 경로/헤더 기반 라우팅(http:)은 불가능. sniHosts가 라우팅 키다.

6.5 적용 + 검증(분석)

CTX=homelab; NS=mesh-test

# 적용 전 서버측 dry-run + 분석
kubectl --context $CTX apply --dry-run=server -f serviceentry-httpbin-ext.yaml \
  -f gateway-egress.yaml -f destinationrule-egress.yaml -f virtualservice-egress.yaml

kubectl --context $CTX apply -f serviceentry-httpbin-ext.yaml \
  -f gateway-egress.yaml -f destinationrule-egress.yaml -f virtualservice-egress.yaml

istioctl --context $CTX analyze -n $NS    # 경고 0 기대

7. 테스트 진행 방법 (검증)

egress의 "완료 정의"는 호출이 200이 아니라 트래픽이 egress gateway를 실제로 경유했는가다. 세 가지를 교차 확인한다: ① 호출 결과 ② Envoy 설정 반영 ③ egress gateway access log.

7.1 ① 호출 — 200 확인

kubectl --context homelab -n mesh-test exec deploy/sleep -c sleep -- \
  curl -sS -o /dev/null -w "%{http_code}\n" https://httpbin.org/get
#  200

7.2 ② 경유 증명 — egress gateway access log

가장 직접적인 증거. 호출 직전 로그를 follow 해두고 호출한다.

# 터미널 A: egress gateway 로그 follow
kubectl --context homelab -n istio-system logs -f deploy/istio-egressgateway

# 터미널 B: 호출 몇 번
bash scripts/traffic.sh https://httpbin.org/get 5    # repo 스크립트
#   또는 위 curl 반복

# 터미널 A 로그에 httpbin.org:443 향 라인이 찍히면 = egress gateway 경유 성공
#   "... httpbin.org:443 ... outbound|443||httpbin.org ..."

passthrough 로그는 TCP 포맷이다. TLS를 풀지 않으므로 egress gateway는 L7을 못 본다. 이 access log 라인은 SNI(requested_server_namebytes_sent/received·duration·response_flags(예: -, UF, UH) 같은 L4 필드만 있고, HTTP method/path/status없다(암호문). 로그에서 HTTP status를 찾다가 "L7이 안 보인다"에서 막히는 게 정상 — L7 가시성이 필요하면 TLS origination(§8)으로 바꿔야 하며, 모드별 가시성은 정본 egress gateway 개념 정본 §07(TLS 모드 가시성) 참조.

로그에 아무것도 안 찍히면 트래픽이 gateway를 안 거치고 sidecar가 직접 나간 것(=라우팅 미스). §9 참조.

7.3 ② 경유 증명 — proxy-config (Envoy에 실제 반영됐는지)

# sleep sidecar가 httpbin.org:443 을 egress gateway 클러스터로 보내도록 프로그래밍됐는지
istioctl --context homelab proxy-config routes deploy/sleep.mesh-test | grep -i httpbin
istioctl --context homelab proxy-config clusters deploy/sleep.mesh-test | grep -i egress

# egress gateway 쪽에 httpbin.org 향 cluster가 생겼는지
istioctl --context homelab proxy-config clusters deploy/istio-egressgateway.istio-system | grep -i httpbin

# 일괄 덤프 (repo 스크립트)
bash scripts/proxy-dump.sh sleep.mesh-test

7.4 ③ 차단 검증 — REGISTRY_ONLY

여기까지는 ALLOW_ANY라 "경유는 하지만, 안 거쳐도 나가긴 한다." egress 통제를 강제하려면 미등록 외부를 막아야 한다. 메시 전역 또는 네임스페이스 SidecarREGISTRY_ONLY 전환.

방법 A — 네임스페이스 한정(Sidecar 리소스, 권장: 영향 범위 작음):

# sidecar-registry-only.yaml — mesh-test 네임스페이스만 REGISTRY_ONLY
apiVersion: networking.istio.io/v1
kind: Sidecar
metadata:
  name: default
  namespace: mesh-test
spec:
  outboundTrafficPolicy:
    mode: REGISTRY_ONLY
kubectl --context homelab apply -f sidecar-registry-only.yaml

방법 B — 메시 전역(values-istiod.yaml의 주석 해제 후 helm 재적용):

meshConfig:
  outboundTrafficPolicy:
    mode: REGISTRY_ONLY

전환 후 테스트:

# 등록된 외부(httpbin.org) -> 여전히 200 (egress gateway 경유)
kubectl --context homelab -n mesh-test exec deploy/sleep -c sleep -- \
  curl -sS -o /dev/null -w "registered  -> %{http_code}\n" https://httpbin.org/get
#  registered  -> 200

# 차단 전제 확인: example.com이 PassthroughCluster가 아니라 BlackHole로 가는지(=실제 차단됨)
istioctl --context homelab proxy-config cluster deploy/sleep.mesh-test | grep -iE 'example|PassthroughCluster|BlackHole'
#  PassthroughCluster 가 잡히고 example.com 전용 cluster가 없으면 -> REGISTRY_ONLY가 덜 적용됨(ALLOW_ANY 잔재)
#  이 경우 example.com 호출이 200으로 새어 나가므로 "확실한 미등록 차단" 검증이 깨진다 -> §7.4 모드 재적용 확인

# 미등록 외부(example.com) -> 차단 (ServiceEntry 없음)
kubectl --context homelab -n mesh-test exec deploy/sleep -c sleep -- \
  curl -sS -o /dev/null -w "unregistered-> %{http_code}\n" --max-time 5 https://example.com
#  unregistered-> 000

왜 미등록이 000인가: REGISTRY_ONLY + passthrough(TLS)에서는 미등록 SNI가 BlackHoleCluster로 가고 Envoy가 연결을 즉시 reset 한다 → curl이 곧바로 000(연결 실패). 이 경로에선 --max-time 5가 발동할 일이 없다. 반대로 SNI 매칭은 됐지만 upstream이 무응답(hang) 하는 경우엔 --max-time 5가 5초 뒤 끊어 000을 만든다 — 둘 다 000이지만 원인 경로가 다르므로 --max-time은 hang 보호용으로 남긴다. 평문 HTTP였다면 같은 BlackHole이라도 L7 응답 502가 뜬다. 즉 프로토콜(L4 reset vs L7 502)에 따라 차단 신호가 다르다.

7.5 합격 기준 (시나리오 완료 정의)

검증 기대
등록 외부 호출 200
egress gateway access log httpbin.org:443 라인 기록됨(= 경유 증명)
proxy-config routes (sleep) httpbin.org → egress gateway cluster
REGISTRY_ONLY + 미등록 외부 차단(000/502)
istioctl analyze -n mesh-test 경고 0

결과는 repo docs/test-reports/2026-06-02_egress.md에 (기대 vs 실제 + 재현 명령) 기록 권장.


8. 대안 패턴 — TLS Origination (앱은 HTTP, gateway가 TLS 시작)

이 부분(passthrough vs TLS/mTLS origination의 개념·트레이드오프 비교, 모드별 가시성, 어느 쪽을 택할지)은 개념 정본과 중복이므로 생략 — 정본: egress gateway 개념 정본 §03(두 모델 결정 규칙)·§06(HTTP + mTLS origination 전체 CRD)·§07(TLS 모드 정밀 비교), 그리고 HTTP vs HTTPS egress 비교 참조.

본 가이드는 요청 시나리오("외부 HTTPS 통신" = 앱이 https://를 직접 호출)에 맞춰 passthrough를 메인으로 두고 homelab에서 구성·검증한다. origination이 필요하면(파트너 mTLS 중앙관리, gateway L7 감사) 정본 §06의 CRD 5종을 본 가이드의 repo/검증 절차에 그대로 대입하면 된다.


9. 트러블슈팅

증상 원인 확인/해결
호출은 200인데 egress 로그가 빔 트래픽이 gateway 안 거치고 sidecar 직접 송신 VirtualService의 hop 1 match(gateways:[mesh], sniHosts)가 맞는지. REGISTRY_ONLY로 바꾸면 경유 강제됨
등록 외부도 차단(000) Gateway selector ↔ egress pod 라벨 불일치 kubectl -n istio-system get pod -l istio=egressgateway 와 Gateway selector.istio 비교
503 UH(no healthy upstream) DNS resolution 실패 / ServiceEntry resolution 오류 ServiceEntry resolution: DNS, 호스트 철자, 노드에서 DNS 되는지
미등록인데 안 막힘 ALLOW_ANY(기본) 상태 Sidecar/mesh REGISTRY_ONLY 적용했는지(§7.4)
sleep 1/2 sidecar 미주입 ns 라벨 istio-injection=enabledrollout restart
proxy-config에 httpbin.org cluster 없음 istiod 미동기 istioctl proxy-status로 SYNCED 확인, analyze

진단 1차 루틴:

istioctl --context homelab proxy-status                       # xDS sync 상태
istioctl --context homelab analyze -n mesh-test               # 설정 정합성
bash scripts/proxy-dump.sh sleep.mesh-test                    # sleep Envoy 전체 덤프
kubectl --context homelab -n istio-system logs deploy/istio-egressgateway --tail=50

10. 정리(cleanup)

# egress 시나리오 객체만 제거 (gateway deployment·istiod는 보존)
kubectl --context homelab -n mesh-test delete \
  serviceentry/httpbin-ext gateway/istio-egressgateway \
  destinationrule/egressgateway-for-httpbin \
  virtualservice/direct-httpbin-through-egress \
  sidecar/default --ignore-not-found

# REGISTRY_ONLY를 mesh 전역으로 켰다면 values 원복 후 helm 재적용 필요

egress gateway deployment/Helm release 자체 제거는 메시 영향 위험 작업 → CLAUDE.md §6에 따라 별도 승인.


핵심 정리

머릿속 한 문장으로 되감으면: choke point로 모으되 TLS는 풀지 않는다 — 그러니 gateway는 SNI만 보고 2-홉으로 라우팅하고, "완료"는 200이 아니라 경유의 증명이다. 이 한 문장에서 아래가 전부 따라온다.


What you might be missing

개념 차원의 주의점(egress gateway는 강제 장치가 아님, passthrough의 L7 가시성 한계, resolution: DNS의 노드 DNS 의존성·주기, Sidecar 리소스의 scope 축소 이중 역할)은 개념 정본과 중복이므로 생략 — 정본: egress gateway 개념 정본 §02(강제 계층 등식)·§04(passthrough 한계·DNS·SNI 위조)·§07(TLS 모드 가시성), sidecar scope 참조.

아래는 이 homelab 가이드를 사내 1.27 메시로 옮길 때의 deltahomelab CNI(Calico) 특이점만 남긴다.


12. 참조

See also


관련 파일 (실제 IaC)

관련 검증Egress mTLS 리포트 — HTTPS over mTLS (ISTIO_MUTUAL)