--- type: guide tags: [istio, egress, gateway, tls-passthrough, sni] created: 2026-06-07 --- # Istio 1.30 Egress Gateway — 외부 HTTPS 통신 설치·구성·테스트 가이드 > [!abstract] > 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 개념 정본](ref__src-egress-gateway.html) > §01·§02·§04에 있다. 본 가이드는 그 개념을 homelab에서 **실제로 구성·검증**하는 절차에 집중하므로 > 이론은 정본에 위임하고 여기서는 "왜 이 객체가 이 모양인지"만 짚는다. --- ## 1. 핵심 아키텍처 — 한 장의 그림과 그로부터 따라오는 모든 것 **머릿속 앵커 한 문장**: choke point로 모으되 **TLS는 절대 풀지 않는다**. 이 한 가지 제약이 이후 모든 설계를 결정한다. ```mermaid flowchart TD subgraph mesh["namespace: mesh-test (injection ON)"] sleep["sleep (curl)"] sc["sleep sidecar (Envoy)\n:15001 outbound capture"] sleep -->|"(0) 15001 outbound capture\nSNI only readable, payload encrypted"| sc end subgraph sys["namespace: istio-system"] egw["istio-egressgateway (Envoy, ClusterIP)\nsingle egress choke point"] end ext["httpbin.org:443 (external)"] sc -->|"hop1: mesh -> egress (SNI)\nend-to-end TLS (ciphertext kept)"| egw egw -->|"hop2: egress -> external (PASSTHROUGH)\nend-to-end TLS (ciphertext kept)"| ext ``` 그림이 말하는 핵심은 **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](rpt__report-2026-06-07_ingress-egress.html)). 먼저 확인: ```bash 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` — 이미 존재. 외부 참조용으로 전문 수록. ```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 설치 / 갱신 ```bash 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 설치 검증 ```bash # 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 배포 ```yaml # namespace.yaml — sidecar 자동 주입 활성화 (이게 있어야 egress 통제가 가능) apiVersion: v1 kind: Namespace metadata: name: mesh-test labels: istio-injection: enabled ``` ```yaml # 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 } ``` ```bash kubectl --context homelab apply -f namespace.yaml -f sleep.yaml # repo라면: make apps (또는 kubectl apply -f scenarios/00-sample-apps/) ``` ### 4.2 주입 확인 (가장 흔한 함정) ```bash 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이다. ```bash 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` 매칭) | ```mermaid flowchart LR SE["ServiceEntry\nhttpbin-ext"] GW["Gateway (egress)\nselector istio=egressgateway"] DR["DestinationRule\negressgateway subset"] VS["VirtualService\n2-hop tls routing"] SE -. "registry (whitelist)" .-> VS VS -->|"hop1: mesh -> egress (SNI)"| GW VS -->|"hop2: egress -> httpbin.org:443"| SE DR -. "hop1 destination subset" .-> VS ``` 추가(차단 검증용): - **`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 개념 정본](ref__src-egress-gateway.html) §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 — 외부 호스트 등록 ```yaml # 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 ```yaml # 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 ```yaml # 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 라우팅 (핵심) ```yaml # 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 적용 + 검증(분석) ```bash 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 확인 ```bash 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 해두고 호출한다. ```bash # 터미널 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 필드만** 있고, HTTP `method`/`path`/`status`는 **없다**(암호문). 로그에서 HTTP status를 찾다가 "L7이 안 > 보인다"에서 막히는 게 정상 — L7 가시성이 필요하면 TLS origination(§8)으로 바꿔야 하며, 모드별 가시성은 > 정본 [egress gateway 개념 정본](ref__src-egress-gateway.html) §07(TLS 모드 가시성) 참조. 로그에 **아무것도 안 찍히면** 트래픽이 gateway를 안 거치고 sidecar가 직접 나간 것(=라우팅 미스). §9 참조. ### 7.3 ② 경유 증명 — proxy-config (Envoy에 실제 반영됐는지) ```bash # 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` 리소스, 권장: 영향 범위 작음):** ```yaml # 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 ``` ```bash kubectl --context homelab apply -f sidecar-registry-only.yaml ``` **방법 B — 메시 전역(`values-istiod.yaml`의 주석 해제 후 helm 재적용):** ```yaml meshConfig: outboundTrafficPolicy: mode: REGISTRY_ONLY ``` 전환 후 테스트: ```bash # 등록된 외부(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 개념 정본](ref__src-egress-gateway.html) > §03(두 모델 결정 규칙)·§06(HTTP + mTLS origination 전체 CRD)·§07(TLS 모드 정밀 비교), > 그리고 [HTTP vs HTTPS egress 비교](ref__src-http-vs-https.html) 참조. > > 본 가이드는 요청 시나리오("외부 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차 루틴: ```bash 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) ```bash # 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`(라우팅 차단) + `Sidecar` scope 축소 + **L3/L4 네트워크 정책**까지 3계층. - **라벨 정렬이 생명줄**: values `labels.istio: egressgateway` == Gateway `selector.istio: egressgateway`. 어긋나면 에러 없이 조용히 트래픽이 안 흐른다. - **검증은 교차 3종**: 호출 200 + egress access log에 `httpbin.org:443` 라인 + proxy-config에 cluster 반영. 셋이 다 맞아야 "경유 증명"이 성립한다. - **차단 신호는 프로토콜에 따라 다르다**: passthrough(L4)에서 미등록은 BlackHole로 즉시 reset → `000`, 평문 HTTP였다면 같은 BlackHole이라도 L7 `502`. --- ## What you might be missing > 개념 차원의 주의점(egress gateway는 강제 장치가 아님, passthrough의 L7 가시성 한계, > `resolution: DNS`의 노드 DNS 의존성·주기, `Sidecar` 리소스의 scope 축소 이중 역할)은 개념 정본과 중복이므로 > 생략 — 정본: [egress gateway 개념 정본](ref__src-egress-gateway.html) > §02(강제 계층 등식)·§04(passthrough 한계·DNS·SNI 위조)·§07(TLS 모드 가시성), > [sidecar scope](mm__src-sidecar-scope.html) 참조. > > 아래는 **이 homelab 가이드를 사내 1.27 메시로 옮길 때의 delta**와 **homelab CNI(Calico) 특이점**만 남긴다. - **CNI 강제 계층은 Calico로 대치**: egress gateway는 그 자체로 송신을 막지 못하고 세 계층을 함께 걸어야 강제된다 — ① mesh `REGISTRY_ONLY`(라우팅 차단) ② `Sidecar` scope 축소(워크로드가 임의 외부 cluster를 모르게) ③ **L3/L4 네트워크 정책으로 egress pod 외 직접 송신 차단**. 정본 §02는 ③을 **Cilium NetworkPolicy + 노드 라우팅** 전제로 기술하지만, **homelab의 실제 CNI는 Calico**이므로 ③은 `CiliumNetworkPolicy`가 아니라 **Calico `NetworkPolicy`/`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 개념 정본](ref__src-egress-gateway.html) §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 개념 정본](ref__src-egress-gateway.html) — 왜/모드결정/2-leg/강제계층의 정본 - [HTTP vs HTTPS egress](ref__src-http-vs-https.html) — passthrough vs origination 프로토콜별 차이 - [egress 운영](ref__src-operations.html) — 운영·진단 관점 - [sidecar scope](mm__src-sidecar-scope.html) · [sidecar scope 노트](mm__note-sidecar-scope.html) — REGISTRY_ONLY/scope 축소 - [east-west gateway SNI](../istio/gw__note-eastwest-gateway-sni.html) — SNI 기반 라우팅 메커니즘 - [DNS resolution 리포트](rpt__report-2026-06-07_dns-resolution.html) — `resolution: DNS` 노드 DNS 의존성 --- ## 관련 파일 (실제 IaC) - 📎 [values-egress-gateway.yaml](../istio/attachment/install/helm/values-egress-gateway.yaml) · 📎 [gateway-egress.yaml](../istio/attachment/scenarios/20-egress/gateway-egress.yaml) - 📎 [serviceentry-httpbin-ext.yaml](../istio/attachment/scenarios/20-egress/serviceentry-httpbin-ext.yaml) · 📎 [20-egress/README.md](../istio/attachment/scenarios/20-egress/README.md) · 📎 [traffic.sh](../istio/attachment/scripts/traffic.sh) - 설치 선행: [Helm 재설치 런북](../istio/arch__runbook-helm-reinstall.html) **관련 검증** → [Egress mTLS 리포트 — HTTPS over mTLS (ISTIO_MUTUAL)](rpt__report-2026-06-08_egress-mtls.html)