Istio 1.30 Egress Gateway — 외부 HTTPS 통신 설치·구성·테스트 가이드
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의 outboundTrafficPolicy가 ALLOW_ANY이면
각 워크로드의 Envoy가 모르는 목적지를 PassthroughCluster로 그냥 흘려보낸다 — pod마다 인터넷으로 나가는
구멍이 하나씩 생기는 셈이다. 이게 운영에서 곤란한 이유는 세 가지다:
- 방화벽 화이트리스트가 불가능 — 송신 출발 IP가 노드 수만큼, pod 수만큼 흩어진다. "이 IP에서만 나간다"를 외부 방화벽에 적을 수가 없다.
- 송신 감사가 불가능 — 누가 어디로 나갔는지 한 곳에 로그가 없다.
- egress 정책 강제 지점이 없다 — "결제 워크로드만 PG사로 나갈 수 있다" 같은 규칙을 걸 단일 지점이 없다.
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는 절대 풀지 않는다. 이 한 가지 제약이 이후 모든 설계를 결정한다.
그림이 말하는 핵심은 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의 핵심 선택:
service.type: ClusterIP— egress는 외부에서 들어오는 트래픽이 없다. 메시 내부 트래픽만 받아 외부로 내보내므로 NodePort/LoadBalancer로 노출할 이유가 없다. (ingress는 외부 인입이므로 NodePort)- 포트
15443(tls) 포함 — SNI 기반 라우팅에 쓰이는 Istio 표준 TLS 포트. (이 가이드 메인 시나리오는 443에 TLS PASSTHROUGH server를 직접 정의하므로 15443은 선택이지만, 표준 구성으로 열어둔다.)
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 매칭) |
추가(차단 검증용):
- 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에 둔다. Gatewayselector는 네임스페이스를 가로질러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, DRegressgateway-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가 아니라 L4TLS프로토콜로 등록하고, 라우팅 키는 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_name)·bytes_sent/received·duration·response_flags(예:-,UF,UH) 같은 L4 필드만 있고, HTTPmethod/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 통제를 강제하려면 미등록 외부를
막아야 한다. 메시 전역 또는 네임스페이스 Sidecar로 REGISTRY_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=enabled → rollout 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이 아니라 경유의 증명이다. 이 한 문장에서 아래가 전부 따라온다.
- 2-홉의 정체: 외부 호출이 sidecar→외부로 직접 안 가고 일부러 한 번 더 꺾여 egress gateway를 경유 (hop1 mesh→gateway, hop2 gateway→external). 이 "일부러 꺾기"가 choke point를 만든다.
- SNI가 유일한 라우팅 키: passthrough라 봉투(TLS)를 안 뜯으니 평문 헤더가 없다. 그래서 ServiceEntry는
protocol: TLS, VirtualService는tls:/sniHosts, 로그는 L4(status/path 없음)다 — 전부 같은 제약의 결과. - gateway만으로는 강제가 안 된다: egress gateway가 떠 있어도
ALLOW_ANY면 sidecar가 그냥 직접 나간다. 강제하려면REGISTRY_ONLY(라우팅 차단) +Sidecarscope 축소 + L3/L4 네트워크 정책까지 3계층. - 라벨 정렬이 생명줄: values
labels.istio: egressgateway== Gatewayselector.istio: egressgateway. 어긋나면 에러 없이 조용히 트래픽이 안 흐른다. - 검증은 교차 3종: 호출 200 + egress access log에
httpbin.org:443라인 + proxy-config에 cluster 반영. 셋이 다 맞아야 "경유 증명"이 성립한다. - 차단 신호는 프로토콜에 따라 다르다: passthrough(L4)에서 미등록은 BlackHole로 즉시 reset →
000, 평문 HTTP였다면 같은 BlackHole이라도 L7502.
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 메시로 옮길 때의 delta와 homelab CNI(Calico) 특이점만 남긴다.
-
CNI 강제 계층은 Calico로 대치: egress gateway는 그 자체로 송신을 막지 못하고 세 계층을 함께 걸어야 강제된다 — ① mesh
REGISTRY_ONLY(라우팅 차단) ②Sidecarscope 축소(워크로드가 임의 외부 cluster를 모르게) ③ L3/L4 네트워크 정책으로 egress pod 외 직접 송신 차단. 정본 §02는 ③을 Cilium NetworkPolicy + 노드 라우팅 전제로 기술하지만, homelab의 실제 CNI는 Calico이므로 ③은CiliumNetworkPolicy가 아니라 CalicoNetworkPolicy/GlobalNetworkPolicy의 egress 규칙으로 구현해야 한다(예: 워크로드 pod의 egress를 egress gateway pod로만 허용하고 그 외 0.0.0.0/0 차단). 정본의 Cilium 언급은 Calico로 대치해 읽을 것 — sidecar를 우회하는 root/hostNetwork 경로는 어느 CNI든 L3 정책 없이는 막히지 않는다. -
버전 정합: egress gateway 버전은 istiod와 맞춰야 한다(여기선 둘 다 1.30.0). gateway가 istiod보다 높으면 xDS 호환 문제, 낮으면 신규 필드 미지원. 사내 기존 메시가 1.27.x이므로, 본 1.30 가이드를 그대로 옮기기 전 istiod부터 정렬할 것.
- 이중 홉의 비용 + 홈랩 단일 장애점: 모든 외부 호출이 Envoy를 두 번(앱 sidecar + egress gateway) 통과한다.
이 가이드의
values-egress-gateway.yaml은replicaCount: 1이라 홈랩에선 egress gateway가 단일 장애점이다. 사내 적용 시 HA(replica↑, HPA, PodDisruptionBudget) + 노드 배치를 반드시 설계 — 노드 핀닝·가용성 트레이드오프는 정본 egress gateway 개념 정본 §08 참조.
12. 참조
- repo:
install/helm/values-egress-gateway.yaml,scenarios/20-egress/README.md,scripts/{traffic,proxy-dump}.sh - runbook:
docs/runbooks/2026-06-01_istio-1.30-helm-reinstall.md(설치 선행 작업) - Istio 공식: "Egress Gateways" / "Egress Gateways for HTTPS Traffic"(SNI passthrough), "Egress TLS Origination"
See also
- egress gateway 개념 정본 — 왜/모드결정/2-leg/강제계층의 정본
- HTTP vs HTTPS egress — passthrough vs origination 프로토콜별 차이
- egress 운영 — 운영·진단 관점
- sidecar scope · sidecar scope 노트 — REGISTRY_ONLY/scope 축소
- east-west gateway SNI — SNI 기반 라우팅 메커니즘
- DNS resolution 리포트 —
resolution: DNS노드 DNS 의존성