🏠 목록 Egress Gateway 필드 매뉴얼 — HTTPS passthrough 중심 (출처: LLM 답변 + Istio 1.30 공식 문서) 📄 MD 원본 🌓 테마
istioegresspassthroughmtlstls-originationservice-mesh

Egress Gateway 필드 매뉴얼 — HTTPS passthrough 중심 (출처: LLM 답변 + Istio 1.30 공식 문서)

ℹ 이 문서가 다루는 것

머릿속에 둘 단 하나의 그림(ANCHOR): egress gateway는 외부 트래픽을 유도하는 라우팅 수렴점일 뿐 강제 장치가 아니며, 외부 구간의 TLS를 누가 종단하느냐가 게이트웨이의 가시성·앱 변경·암호화 범위를 한꺼번에 결정한다. 앱이 끝까지 TLS를 쥐면(https) 게이트웨이는 SNI만 보는 passthrough(주류), 앱이 평문을 보내면(http) 게이트웨이가 TLS를 대신 맺는 origination(특수) — 이 한 갈래에서 나머지(가시성·앱 변경·암호화·인증서 위치)가 전부 따라 나온다. - Sidecar 모드 (Ambient 아님) · CRD는 Istio 1.30 / networking.istio.io/v1 기준 (공식 문서 검증, 출처는 문서 끝) - 주류 경로 = 앱이 https://로 직접 호출 → 게이트웨이 PASSTHROUGH (섹션 04, 가장 비중 큼) - 특수 경로 = 앱을 http://로 바꿔 게이트웨이가 TLS/mTLS를 대신 맺는 origination (섹션 06, 필요할 때만)

대상환경: Istio 1.30 sidecar mesh · on-prem/홈랩(Calico CNI 전제) · 대상독자: egress 경로를 설계·운영하는 DevOps/SRE · 범위: 설계·TLS 모델 정본(운영 deep-dive는 egress 운영 정본에 위임) · 선행개념: 아래 박스.

ℹ 선행 개념 — 이 7개만 손에 쥐면 본문이 풀린다

본문은 이 단어들 위에서 돈다. 모르는 것만 집어 읽고 넘어가라. - TLS / 종단(termination): 주고받는 내용을 암호화하는 규약(https의 's'). 암호를 푸는 지점이 종단인데, 한 연결에서 종단은 단 한 번뿐이다 — 이 제약이 passthrough vs origination을 가르는 물리 법칙이다. - SNI: TLS를 열 때 평문으로 같이 실리는 목적지 도메인(예: api.partner.example.com). 암호를 못 풀어도 "어느 호스트로 가는 연결인지"는 보인다 → passthrough 라우팅의 유일한 단서. - passthrough vs origination: 전자는 암호를 안 풀고 SNI만 보고 통과, 후자는 게이트웨이/사이드카가 외부행 TLS를 직접 새로 맺음. - L4 / L7: L4 = 연결(TCP) 수준("어디로 얼마나"), L7 = 요청(HTTP) 수준("어떤 요청이 200이었나"). 게이트웨이가 L7을 보려면 암호를 풀어야 한다. - 사이드카 vs egress gateway: 사이드카 = 앱마다 붙는 Envoy(모든 트래픽 통과). egress gateway = 외부행만 모으는 전용 Envoy. - 메시 자동 암호화 (mesh mTLS, ISTIO_MUTUAL): 클러스터 안 사이드카끼리를 Istio가 자동 양방향 암호화하는 것. 외부 파트너와의 mTLS와는 다른 구간, 다른 인증서다(혼동이 모든 오해의 출발점). - 설정 리소스 4종: ServiceEntry(어떤 외부를 허용·등록) · Gateway(게이트웨이가 받는 포트) · VirtualService(어디로 보낼지 규칙) · DestinationRule(보낸 뒤 TLS·묶음 정책).


01. 배경 — 왜 egress gateway가 존재하나 (해결하는 문제)

사이드카를 주입하면 기본값(outboundTrafficPolicy: ALLOW_ANY)에서 각 Pod는 자기 사이드카를 거쳐 외부로 직접 나간다 — 출구가 노드 수만큼 흩어지고, 누가 어디로 나갔는지 한 곳에 안 남으며, 파트너에게 등록할 출구 IP도 고정되지 않는다. egress gateway가 푸는 문제는 이 흩어진 외부행을 하나의 제어점(choke point)으로 수렴시키는 것이고, 거기서 네 가지가 파생된다.

동기 무엇을 얻는가
노드 고정 (가장 흔한 목표) 외부 NAT·방화벽 통로를 특정 노드에만 부여. 앱 노드는 외부 라우트가 없고 egress 노드만 인터넷에 도달. 출구 IP를 고정해 파트너 IP allowlist에 등록 가능
관측·감사 외부 접근이 단일 지점을 통과 → 누가·어디로·얼마나 나갔는지 한 곳에서 수집. PCI-DSS 감사 추적에 직접 연결
인증서 집중 파트너가 client 인증서(mTLS)를 요구할 때, 모든 앱이 각자 들고 있는 대신 게이트웨이 한 곳에 모음. 단, 이건 origination(섹션 06)에서만
공격면 축소 침해된 Pod의 데이터 유출 경로를 게이트웨이로 한정. 단, 강제 계층이 따로 받쳐줄 때만 — 섹션 02

대부분의 도입 목적은 첫 줄 — "외부행을 특정 노드로만" — 이다. 그리고 그 경우 앱은 보통 이미 https://로 외부를 부르고 있어서, 앱을 건드리지 않고 게이트웨이만 얹는 passthrough가 자연스러운 선택이 된다.

ℹ What you might be missing

egress gateway는 ingress gateway의 거울상이 아니다. Ingress는 외부 트래픽이 물리적으로 그 LoadBalancer IP를 거칠 수밖에 없어 자연히 강제된다. Egress엔 그런 물리적 강제가 없다 — Pod는 여전히 자기 사이드카에서 임의 목적지로 나갈 수 있다. 공식 문서도 명시한다: "egress Gateway를 정의하는 것 자체는 그 게이트웨이가 도는 노드에 어떤 특별한 취급도 부여하지 않는다." 이 비대칭성이 egress 운영 난이도의 근원이다.


02. 전제 교정 — 게이트웨이는 강제 장치가 아니다

도입 사고가 가장 많이 나는 지점이라 먼저 못을 박는다. "트래픽을 게이트웨이로 라우팅""트래픽이 게이트웨이를 거치도록 강제" 는 완전히 다른 레이어의 일이다.

VirtualService로 "이 호스트는 egress gateway를 거쳐라"라고 쓰면, 그 규칙을 따르는 사이드카는 게이트웨이로 보낸다. 하지만 규칙에 안 걸리는 호스트, 사이드카가 없는 Pod, 사이드카를 우회하도록 조작된 컨테이너는 그냥 직접 나간다. 기본값 ALLOW_ANY에서는 규칙에 없는 목적지를 PassthroughCluster로 조용히 통과시켜 버린다. 즉 "조용한 우회"가 기본 동작이다.

진짜로 "특정 노드로만"을 보장하려면 세 계층이 함께 서야 한다.

계층 메커니즘 역할 / 한계
① L7/SNI 라우팅 VirtualService mesh→gateway 외부행을 게이트웨이로 유도. 규칙을 따르는 사이드카에만 적용. 우회 차단 불가
② 레지스트리 통제 outboundTrafficPolicy: REGISTRY_ONLY 등록(ServiceEntry)되지 않은 목적지를 BlackHoleCluster로 차단. "알려진 외부만". 등록된 외부로는 여전히 직접 나갈 수 있음
③ 네트워크 강제 NetworkPolicy(CNI의 egress 정책) + 노드 라우팅 앱 노드의 0.0.0.0/0 아웃바운드 차단, 외부 NAT는 egress 노드에만. 여기서 비로소 물리적 강제
✓ 강제의 핵심 등식

실질적 강제 = (REGISTRY_ONLY로 미등록 차단) × (NetworkPolicy로 직접 아웃바운드 차단) × (egress 노드만 NAT 보유). 게이트웨이 라우팅은 그 위에 얹는 "정상 경로"일 뿐. 셋 중 하나라도 빠지면 우회 구멍이 남는다. Istio 단독으로는 ③을 못 만든다 — CNI의 egress NetworkPolicy(Cilium은 CiliumNetworkPolicy, Calico는 GlobalNetworkPolicy)가 ③을 담당한다. ⚠️ 단, Calico는 egress 정책 표현력에 제약이 있어(도메인 기반 egress 등 일부 기능 미지원·제한) 노드 라우팅·외부 NAT 한정에 더 의존하게 된다 — 홈랩 CNI가 Calico임을 전제로 검증할 것.

ℹ What you might be missing

REGISTRY_ONLY를 메시 전역에 켜는 순간, ALLOW_ANY에 기대 조용히 나가던 외부 호출이 한꺼번에 막힌다. 로그 수집기, 메트릭 푸시, 외부 시크릿 매니저, OCSP/CRL 인증서 검증, 컨테이너 레지스트리 — 평소 안 보이던 의존성이 전부 ServiceEntry를 요구한다. air-gapped라도 내부 미러·프록시로 가는 트래픽까지 레지스트리에 있어야 한다. 그래서 전역 적용은 "켜고 본다"가 아니라, Sidecar 리소스로 네임스페이스 단위 audit → 트래픽 인벤토리 확보 → 점진 확대가 정석이다.


03. 핵심 아키텍처 — 갈래는 단 하나, "TLS를 누가 종단하나"

여기가 이 문서의 심장이다. 선행 박스의 물리 법칙 — 한 연결에서 TLS 종단은 단 한 번 — 을 egress 경로에 적용하면 모든 설계가 한 갈래에서 갈라진다: 외부행 TLS를 앱이 쥐느냐(passthrough), 게이트웨이가 쥐느냐(origination). 이 하나를 정하는 순간 게이트웨이의 가시성·앱 변경·암호화 범위·인증서 위치가 자동으로 결정된다 — 따로 고를 자유도가 아니라, TLS 종단점을 정하면 따라 나오는 종속 변수들이다. 그래서 설정·모니터링을 외우기 전에 이 한 줄부터 정해야 한다.

주류 · HTTPS + passthrough 특수 · HTTP + origination
앱 호출 https://api.partner... — 앱이 직접 TLS 종단 http://api.partner... — 앱은 평문 전송
게이트웨이 복호화 안 함. SNI만 읽어 라우팅 TLS 종단 후 새 TLS를 대신 맺음(client cert 제시)
암호화 앱 ↔ 외부 end-to-end 그대로 유지 앱↔GW(메시 mTLS) + GW↔외부(새 mTLS), 2구간
앱 변경 없음 — 그대로 두고 게이트웨이만 얹음 필요httpshttp로 호출 변경(침습적)
게이트웨이 L7 가시성 없음 (SNI·L4만) 있음 (method·path·status)
주 용도 "외부행을 특정 노드로", 출구 IP 고정, 감사 파트너 mTLS 중앙관리, 게이트웨이 L7 관측
자세히 '주류 경로' 섹션 (04) '특수 경로' 섹션 (06)
✓ 결정 규칙

앱이 이미 https://로 외부를 호출하고 있다면 → 주류(passthrough, 섹션 04). 앱을 그대로 두고 게이트웨이만 얹어 "특정 노드로" 목표를 달성한다. 게이트웨이가 인증서를 대신 관리해야 하거나(파트너 mTLS) 게이트웨이에서 HTTP 레벨 메트릭이 꼭 필요할 때만 → 특수(origination, 섹션 06). 둘은 배타적이지 않아서, 파트너별로 호스트 단위로 섞어 쓸 수 있다.


04. 주류 경로 — HTTPS 직접 호출 + passthrough

앱이 https://api.partner.example.com으로 직접 호출하고, 게이트웨이는 그 TLS를 풀지 않고 통과시키는 형태다. 게이트웨이가 읽는 건 TLS ClientHello에 평문으로 실린 SNI(목적지 호스트명)뿐이다. TLS는 처음부터 끝까지 앱과 외부 사이에서만 종단된다.

APP NODE (no external route) App https :443 Sidecar reads SNI only payload opaque EGRESS NODE (has NAT) Egress Gateway PASSTHROUGH SNI 라우팅 · 복호화 X Partner API TLS 종단 (서버) end-to-end TLS (암호문) 암호문 그대로
그림 1. HTTPS passthrough — 앱↔Partner 사이 end-to-end TLS, 게이트웨이는 SNI만 읽고 복호화하지 않음.

TLS 터널은 앱에서 외부까지 한 번에 이어진다. 게이트웨이는 그 터널을 끊지 않고 SNI만 보고 길을 안내한다. 게이트웨이가 보는 것: SNI + bytes + duration. HTTP method·path·status·header = 안 보임(암호문).

HTTPS passthrough 요청 6단계

단계 무엇이 일어나나 관장 리소스 흔한 실패
1 앱이 HTTPS로 외부 호출 (SNI 포함 ClientHello) (앱 코드) SNI를 안 싣는 클라이언트면 SNI 기반 라우팅 불가
2 사이드카 가로채기 — iptables REDIRECT :15001, SNI만 평문으로 읽힘 (istio-init) excludeOutboundIPRanges 어노테이션이면 우회
3 SNI로 게이트웨이 라우팅 (mesh leg) VirtualService + DestinationRule NRhttp:로 잘못 작성(tls:+sniHosts여야), sniHosts/host mismatch
4 게이트웨이 PASSTHROUGH 수신 — 복호화 없이 SNI 매칭 Gateway server protocol이 TLS 아니거나 mode 누락
5 게이트웨이 → 외부 forward (egress leg + ServiceEntry resolution) VirtualService + ServiceEntry UH — DNS 미해석 / resolution 설정 오류
6 앱 ↔ 외부 end-to-end TLS 완성 (게이트웨이는 중간 도관) (도관) 파트너 인증서/SNI 검증 실패는 앱에서 발생

설정 — CRD 4종 (passthrough)

핵심 차이: ServiceEntry 포트가 protocol: TLS, 게이트웨이가 tls.mode: PASSTHROUGH, 그리고 VirtualService가 http:가 아니라 tls: + sniHosts 로 라우팅한다(SNI 기반 L4 라우팅이라). 전부 istio-system에 둔다.

# ① ServiceEntry — 외부 등록 (protocol: TLS)
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata:
  name: partner-api
  namespace: istio-system
spec:
  hosts:
    - api.partner.example.com
  ports:
    - number: 443
      name: tls
      protocol: TLS          # HTTPS 아님! SNI 기반 passthrough
  resolution: DNS            # DNS / STATIC / NONE — 아래 DNS 절 참고
  location: MESH_EXTERNAL
# ② Gateway — PASSTHROUGH 리스너
apiVersion: networking.istio.io/v1
kind: Gateway
metadata:
  name: egress-partner
  namespace: istio-system
spec:
  selector:
    istio: egressgateway
  servers:
    - port:
        number: 443
        name: tls
        protocol: TLS
      hosts:
        - api.partner.example.com
      tls:
        mode: PASSTHROUGH     # 복호화하지 않고 SNI로만 통과
# ③ DestinationRule — subset 정의만 (TLS 설정 없음)
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: egressgateway-partner
  namespace: istio-system
spec:
  host: istio-egressgateway.istio-system.svc.cluster.local
  subsets:
    - name: partner          # passthrough에선 mTLS 래핑 없음 — subset 이름만
# ④ VirtualService — SNI 라우팅 (tls + sniHosts, 2-leg)
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: route-partner
  namespace: istio-system
spec:
  hosts:
    - api.partner.example.com
  gateways:
    - mesh                   # leg 1: 사이드카에서
    - egress-partner         # leg 2: 게이트웨이에서  (Gateway 이름과 일치!)
  tls:                       # http: 아님 — SNI 기반이므로 tls:
    - match:                 # leg 1 — 메시 → 게이트웨이
        - gateways: [mesh]
          port: 443
          sniHosts: [api.partner.example.com]
      route:
        - destination:
            host: istio-egressgateway.istio-system.svc.cluster.local
            subset: partner
            port:
              number: 443
    - match:                 # leg 2 — 게이트웨이 → 외부
        - gateways: [egress-partner]
          port: 443
          sniHosts: [api.partner.example.com]
      route:
        - destination:
            host: api.partner.example.com
            port:
              number: 443

앱은 평소처럼 https://api.partner.example.com을 부르면 된다. 인증서·TLS는 앱이 끝까지 책임지고, 게이트웨이는 경로만 강제한다. 앱 코드 0 변경 — 이게 passthrough가 주류인 이유다.

변형 — proxy와 게이트웨이 사이를 mTLS로 한 겹 더 감싸기 (권장)

위 기본형은 사이드카↔게이트웨이 구간의 TLS를 명시하지 않았다. 실무에서, 특히 PCI-DSS처럼 "내부 구간도 암호화·인증됐음을 증명"해야 하는 환경에서는, 그 구간을 메시 자동 mTLS(ISTIO_MUTUAL)로 명시해서 한 겹 더 감싼다. 이때도 게이트웨이는 앱의 외부 TLS를 여전히 풀지 않는다 — TLS가 두 겹으로 겹칠 뿐이다.

App Sidecar Egress GW passthrough Partner outer: end-to-end TLS (GW가 못 풂, App 관통 Partner) inner: ISTIO_MUTUAL (Sidecar↔GW 구간만, 메시 인증서·워크로드 신원)
그림 2. 두 겹 TLS — 바깥 봉투(end-to-end, GW가 안 풂)와 안쪽 ISTIO_MUTUAL(Sidecar↔GW 구간만)이 독립적으로 겹친다.

여기엔 서로 독립된 두 개의 스위치가 있다는 게 핵심이다. 하나는 "게이트웨이가 외부 TLS를 푸느냐"(Gateway server 쪽), 다른 하나는 "proxy→게이트웨이 구간을 mTLS로 감싸느냐"(DestinationRule 쪽). 우리가 원하는 건 앞은 안 풀고(PASSTHROUGH) 뒤는 감싸는(ISTIO_MUTUAL) 조합이다.

설정할 것 필드 (리소스)
바깥 봉투(외부 TLS)를 안 푼다 Gateway servers[].tls.mode PASSTHROUGH
안쪽 봉투(proxy→egress)를 mTLS로 감싼다 DestinationRule trafficPolicy.tls.mode ISTIO_MUTUAL
라우팅 (양쪽 leg) VirtualService tls + sniHosts

기본형과 비교해 DestinationRule 한 곳만 바뀐다(ServiceEntry·Gateway·VirtualService는 위 기본형 그대로). subset에 trafficPolicy를 붙여 ISTIO_MUTUAL을 명시한다.

# ③ DestinationRule (변형) — proxy→egress를 ISTIO_MUTUAL로 명시
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: egressgateway-partner
  namespace: istio-system
spec:
  host: istio-egressgateway.istio-system.svc.cluster.local
  subsets:
    - name: partner
      trafficPolicy:
        portLevelSettings:
          - port:
              number: 443
            tls:
              mode: ISTIO_MUTUAL          # proxy(사이드카) -> egress 게이트웨이 mTLS
              sni: api.partner.example.com  # 이 sni는 "게이트웨이로 가는 연결"의 것
⚠ 중요 — 이 sni와 ISTIO_MUTUAL의 위치

ISTIO_MUTUALDestinationRule(proxy→egress hop) 에 들어간다. Gateway servertls.mode를 ISTIO_MUTUAL로 바꾸면 그건 "게이트웨이가 메시 TLS를 종단한다"는 뜻이 되어 origination(특수 경로)으로 넘어간다. 반드시 Gateway server = PASSTHROUGH 고정, DestinationRule = ISTIO_MUTUAL 조합이어야 한다. 또 여기 sni는 게이트웨이로 가는 안쪽 연결의 SNI이지, 외부 파트너로 가는 SNI가 아니다 — 외부행 SNI는 앱이 만든 바깥 봉투에 들어있고 게이트웨이는 그대로 전달한다.

ℹ Auto mTLS와의 관계 — 무엇이 실제로 달라지나

Istio에는 "Auto mTLS"가 있어서, DestinationRule에 TLS를 명시하지 않은 기본형에서도 proxy→게이트웨이 구간은 이미 메시 mTLS일 가능성이 높다. 그러니 ISTIO_MUTUAL을 명시하는 실익은 "없던 암호화를 새로 만드는 것"이 아니라, 자동 추론에 맡기지 않고 mTLS와 SNI를 못 박아 확정하는 데 있다 — air-gapped·PCI 환경에서 "이 구간은 확실히 mTLS다"를 증명·고정해야 할 때 권장된다. 현재 상태는 istioctl proxy-config cluster deploy/istio-egressgateway -n istio-system --fqdn api.partner.example.com -o json에서 transportSocket이 mTLS인지로 확인한다.

이 변형으로 얻는 것: 내부 구간(proxy→게이트웨이)의 암호화 + 워크로드 신원(SPIFFE) 기반 인증·정책. 얻지 못하는 것: 게이트웨이에서의 요청 수준(L7) 가시성 — 바깥 봉투를 안 풀었으니 여전히 0이다. 즉 이 구성은 "앱이 https를 그대로 들고 가되, 내부 hop까지 확실히 암호화·인증된 passthrough"다.

DNS — 어떻게 되어야 하는가

passthrough에서 가장 자주 막히는 지점이라 따로 본다. 호스트명이 IP로 바뀌는 일이 두 군데에서 일어나고, 그 둘은 독립적이다.

app  --DNS query-->  CoreDNS  -->  IP   (app opens TLS, SNI in ClientHello)
                                    |
sidecar  --route by SNI-->  egress gateway
                                    |
gateway  --ServiceEntry.resolution-->  real destination IP
    DNS    : gateway resolves the hostname itself (background, 30s)
    NONE   : gateway reuses the ORIGINAL dest IP (the one app resolved)
    STATIC : gateway uses the endpoints[] IPs you declared

여기서 결정적인 함정 하나: Istio 프록시는 요청 시점에 동기 DNS를 하지 않는다. resolution: DNS면 프록시가 백그라운드 주기로 재해석한 결과를 모든 요청에 쓴다. 기본 주기는 약 30초인데, 이건 고정값이 아니라 meshConfig.dnsRefreshRate(전역) 또는 cluster의 DNS 설정에서 조정되는 값이다 — 즉 IP가 자주 바뀌는 외부엔 이 주기를 줄여 대응할 수 있다. 그래서 파트너 IP가 바뀌어도 최대 (재해석 주기 + DNS TTL)만큼 지연된다.

실제 적용된 주기·DNS 모드는 cluster config_dump로 확인한다.

istioctl proxy-config cluster deploy/istio-egressgateway -n istio-system \
  --fqdn api.partner.example.com -o json \
  | grep -E 'dnsRefreshRate|dnsLookupFamily|respectDnsTtl|type'
# 기대 출력(예): "type": "STRICT_DNS", "dnsRefreshRate": "30s", "respectDnsTtl": true
#  - type=STRICT_DNS  → 프록시가 hostname을 직접 주기 해석(resolution: DNS)
#  - dnsRefreshRate   → 백그라운드 재해석 주기(기본 30s, meshConfig.dnsRefreshRate로 조정)
#  - respectDnsTtl    → DNS TTL을 함께 존중(주기 vs TTL 중 짧은 쪽 영향)
resolution 동작 언제
DNS 게이트웨이가 hostname을 30초 주기로 직접 해석. 게이트웨이 노드가 외부 DNS에 도달 가능해야 단일 호스트, 게이트웨이가 외부 DNS를 쓸 수 있는 환경
NONE 해석 안 함. 앱이 이미 해석한 원래 목적지 IP를 그대로 사용. 게이트웨이는 DNS 불필요 와일드카드(*.partner.com), 앱 해석을 신뢰하는 일반 passthrough
STATIC ServiceEntry의 endpoints[].address에 IP 명시. endpoints 빠지면 동작 안 함 고정 IP, DNS 불안정, air-gapped

air-gapped 환경에서 외부 DNS가 없다면 STATIC + 명시 IP가 가장 단순하고, 내부 forwarding resolver(CoreDNS forward 존)를 두면 DNS도 가능하다. 한편 Istio DNS proxy(ISTIO_META_DNS_CAPTURE: "true")를 켜면 사이드카가 앱의 DNS 질의를 가로채 Istio 레지스트리로 응답하는데, 여기에 auto-allocate(ISTIO_META_DNS_AUTO_ALLOCATE)를 더하면 ServiceEntry 호스트마다 240.240.0.0/16 대역의 가상 IP를 배정해 준다. 기본은 비활성이고, DNS 응답을 바꾸므로 일부 앱과 호환성 이슈가 있을 수 있다.

모니터링 — 무엇이 보이고 무엇이 안 보이나

passthrough의 관측은 "안 되는" 게 아니라 L4까지만 되고 L7이 0이다. 게이트웨이는 암호문을 못 풀기 때문이다.

게이트웨이에서 보이는 것 (L4 + SNI) 안 보이는 것 (L7)
✅ 목적지 SNI(어느 외부 호스트) ❌ HTTP method · path · query
✅ 송수신 바이트, 연결 시간 ❌ status code · 응답 크기
✅ TCP 성공/실패 + response_flags ❌ request/response header
✅ 출발 워크로드 identity ❌ 요청 단위 latency(p50/p99), tracing span

메트릭으로 말하면, 게이트웨이에는 istio_tcp_connections_opened_total·istio_tcp_sent_bytes_total 같은 L4 시리즈만 잡히고, istio_requests_total·istio_request_duration_milliseconds 같은 L7 시리즈는 없다. access log도 HTTP가 아니라 TCP 포맷(SNI·바이트·duration·flags)이다.

ℹ 모니터링 실무 결론

게이트웨이 = "누가·어느 외부로·얼마나·TCP 성공했나"(통제·감사·이상 탐지엔 대개 충분). 앱 계측 = "그 호출이 200이었나, 얼마나 느렸나"(L7 디테일). 게이트웨이에서 중앙집중 L7 메트릭이 꼭 필요하면, 그건 passthrough로는 안 되고, 게이트웨이를 TLS 끝점으로 만들거나 앱을 직접 계측해야 한다 — 그 선택지 전체를 바로 다음 '가시성 옵션' 섹션에서 정리한다.

설정 체크리스트

ℹ What you might be missing
  • SNI는 클라이언트가 평문으로 채워 넣는 값이라 위조 가능하다 — SNI 기반 라우팅·정책은 신뢰 경계가 약하고, 진짜 강제는 강제 섹션의 네트워크 계층(목적지 IP allowlist)에 기대야 한다.
  • resolution: DNS30초 주기 + TTL 때문에 IP가 자주 바뀌는 외부(CDN 뒤의 API 등)는 간헐 실패가 날 수 있다 — 이럴 땐 NONE(앱 해석 신뢰)이 더 안정적이다.
  • 와일드카드 호스트(*.partner.com)는 resolution: NONE과 함께 써야 하고, SNI가 없거나 일치하지 않는 클라이언트는 라우팅이 실패한다.
  • 미래 리스크 — Encrypted Client Hello(ECH) 로 SNI 자체가 암호화되면 SNI 기반 passthrough 라우팅이 깨진다. "게이트웨이는 SNI를 본다"는 전제가 영구하지 않다.

05. 앱이 HTTPS를 보낼 때, 가시성을 얻는 방법

앞 섹션의 상황을 그대로 둔다 — 앱이 https://api.partner.example.com으로 직접 호출한다. 이때 게이트웨이에서 "무엇을 볼 수 있느냐"가 자주 막히는 지점이라, 선택지를 전부 펼쳐 비교한다.

ℹ 먼저: 가시성에는 두 층이 있다

연결 수준(흔히 L4라고 부름) — TCP 연결 하나하나에 대한 정보. 어느 워크로드가, 어느 외부 호스트(SNI)로, 몇 바이트를, 몇 번·얼마 동안 연결했고, 성공/실패했는지. SNI는 암호를 못 풀어도 보인다.

요청 수준(흔히 L7이라고 부름) — HTTP 요청 하나하나에 대한 정보. method, 경로, 응답 코드, 응답 시간, 헤더. 이건 암호문을 풀어야만 보인다.

핵심은 단순하다. 요청 수준(L7)을 보려면 누군가 암호를 풀어야 하고, 암호를 풀 수 있는 건 TLS 연결의 양 끝점(앱, 또는 상대 서버)뿐이다. 앱이 HTTPS로 끝까지 직접 들고 가면 게이트웨이는 끝점이 아니라 중간이라 기본적으로 암호를 못 푼다. 그래서 게이트웨이에서 요청 수준을 보려면 둘 중 하나다 — (가) 게이트웨이를 억지로 TLS 끝점으로 만들어 중간에서 풀거나, (나) 애초에 앱이 암호를 안 쥐게 만들거나.

선택지 한눈에 보기

선택지 앱 호출 게이트웨이가 암호 푸나 게이트웨이 L7 끝까지 암호화 앱 변경 client 인증서 위치
1. passthrough 그대로 https 아니오 없음 유지 없음 앱(필요 시)
2. origination (앱→http) http 해당 없음(앱이 평문) 전부 2구간 분리 필요 게이트웨이
3. 가로채기(bridging) https 전부 깨짐 신뢰목록만 게이트웨이
4. 앱 계측 https 아니오 앱에서만 유지 계측 추가 앱(필요 시)

※ "끝까지 암호화" = 앱과 외부 서버 사이가 한 번도 풀리지 않고 암호화된 채 이어지는 것. 선택지 2·3은 중간에 게이트웨이가 끼어 한 번 풀거나 다시 맺으므로 한 줄 암호화는 아님.

선택지 1 — passthrough 그대로 두기 (연결 수준만)

게이트웨이가 암호를 안 풀고 SNI만 읽어 길을 안내한다. 설정은 주류 경로 섹션의 passthrough 그대로. 추가로 할 일은 접속 기록(access log)을 켜고 거기 남는 SNI·바이트·성공여부로 대시보드를 만드는 것뿐이다.

# 접속 기록 켜기 (전역 meshConfig)
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
  meshConfig:
    accessLogFile: /dev/stdout      # 게이트웨이/사이드카 접속 기록 출력
    # 기본 포맷에 SNI(requested_server_name) 바이트 실패사유가 포함됨

선택지 2 — 앱을 HTTP로 바꾸고 게이트웨이가 TLS를 대신 맺기 (게이트웨이에서 요청 수준 전부)

앱이 http://로 보내면, 사이드카와 게이트웨이 사이 구간은 메시 자동 암호화로 보호되고, 게이트웨이가 그 HTTP를 본 다음 새 TLS를 외부에 맺는다. 게이트웨이가 끝점이 되니 요청 수준이 전부 보인다. 전체 설정은 특수 경로 섹션에 있다.

ℹ 팀이 "HTTPS인데 게이트웨이에서 보인다"고 하면 십중팔구 이것

앱은 사실 http://로 부르는데, 사이드카↔게이트웨이 구간이 메시 자동 암호화(ISTIO_MUTUAL, 일명 mesh mTLS)로 자동 암호화돼서 "전 구간이 HTTPS인 것처럼" 보이는 것. 여기서 "mTLS"는 외부 파트너와의 mTLS가 아니라 메시 내부 자동 암호화를 가리킨다. 이 둘을 섞어 부르면 "HTTPS 직접 + mTLS origination"이라는, 한 연결에서는 성립할 수 없는 표현이 나온다.

선택지 3 — 게이트웨이가 앱의 TLS를 중간에서 풀고 다시 맺기 (가로채기 / bridging)

앱은 https:// 그대로 둔다. 대신 게이트웨이가 그 TLS를 종단(풀고) 해서 HTTP를 본 뒤, 외부로 새 TLS를 맺는다. 흔히 말하는 트래픽 가로채기·검사 프록시와 같은 원리다.

여기엔 까다로운 전제가 있다. 게이트웨이가 앱의 TLS를 풀려면 앱에게 "내가 api.partner.example.com이다"라고 내세울 server 인증서를 제시해야 하고, 앱이 그 인증서를 신뢰해야 한다. 즉 사내 CA로 그 외부 도메인용 인증서를 발급하고, 앱의 신뢰 목록에 그 사내 CA를 심어야 한다. 이건 사실상 의도된 중간자(MITM) 구성이다.

# 구조 — Gateway가 외부 도메인 cert로 TLS 종단 (개념)
apiVersion: networking.istio.io/v1
kind: Gateway
metadata: { name: egress-partner, namespace: istio-system }
spec:
  selector: { istio: egressgateway }
  servers:
    - port: { number: 443, name: https, protocol: HTTPS }
      hosts: [api.partner.example.com]
      tls:
        mode: SIMPLE                          # PASSTHROUGH 아님 — 여기서 종단(복호화)
        credentialName: partner-domain-cert   # 사내 CA가 발급한 그 도메인용 server cert
# 이후 DestinationRule(MUTUAL/SIMPLE)로 외부에 다시 TLS origination
# 그리고 앱 신뢰 목록에 사내 CA를 심어야 앱이 연결을 수락함
⚠ 알려진 함정

게이트웨이에서 TLS를 종단해도 요청 수준(L7) 메트릭이 자동으로 안 잡히는 경우가 있다 — Envoy 리스너가 여전히 TLS 통과용으로 동작해 HTTP로 인식하지 못하기 때문. L7 메트릭을 실제로 얻으려면 종단 후 트래픽을 HTTP로 라우팅하도록 추가 구성(VirtualService를 tls가 아니라 http로)이 필요하고, 이 부분은 환경마다 달라 검증이 필수다.

선택지 4 — 앱에서 직접 계측하기 (앱 쪽에서 요청 수준)

게이트웨이는 passthrough 그대로 두고, 앱이 자기 HTTP 클라이언트의 메트릭·분산 추적(OpenTelemetry 등)을 직접 남기는 방법. 앱이 TLS의 끝점이라 응답 코드·지연을 이미 알고 있다. 암호화를 전혀 깨지 않고 요청 수준을 얻는다 — 단, 게이트웨이 한 곳이 아니라 앱마다 계측이 들어간다.

끝까지 암호화는 지켜야 하고 요청 수준은 필요한데 "게이트웨이 집중"이 필수는 아닐 때, 실무에서 가장 흔한 절충이다. 게이트웨이의 연결 수준(선택지 1) + 앱의 요청 수준(선택지 4)을 합치면 암호화를 안 깨면서도 "어디로 얼마나(게이트웨이)" + "그 호출이 200이었나(앱)"를 모두 얻는다.

✓ 결정 가이드
  • 끝까지 암호화를 깨면 안 되고 게이트웨이 요청수준이 꼭 필요한 건 아니다 → 선택지 1(passthrough) + 선택지 4(앱 계측). 가장 흔하고 안전한 조합.
  • 게이트웨이에서 요청수준이 필요하고 앱을 바꿔도 된다 → 선택지 2(origination). 가장 깔끔하게 게이트웨이 L7을 얻음.
  • 게이트웨이에서 요청수준이 필요한데 앱은 https를 유지해야 하고 복호화가 허용된다 → 선택지 3(가로채기). 사내 CA·신뢰 관리가 전제라 가장 무거움.
ℹ What you might be missing
  • "게이트웨이에서 요청 수준(L7) 가시성"과 "끝까지 암호화"는 동시에 가질 수 없다. 둘 중 하나는 반드시 포기한다 — 이게 모든 선택을 가르는 근본 트레이드오프다.
  • "암호화돼 보인다"가 두 가지를 뜻한다 — 메시 내부 자동 암호화(ISTIO_MUTUAL)외부 파트너와의 TLS 는 다른 구간, 다른 인증서다. 선택지 2에서 "HTTPS처럼 보이는" 건 앞쪽(내부)이지 끝까지가 아니다.
  • 선택지 3은 외부 도메인 인증서를 사내에서 발급해 앱이 신뢰하게 만드는 것이라 감사·규제 관점에서 "우리가 우리 트래픽을 중간에서 깐다"는 사실이 문제가 될 수 있다 — 기술적으로 가능하다고 정책적으로 허용되는 건 아니다.
  • 가시성을 위해 굳이 게이트웨이를 끝점으로 만들 필요가 없는 경우가 많다 — 선택지 4(앱 계측)는 암호화를 안 깨면서 요청 수준을 주는 가장 비침습적인 길이라, "게이트웨이 집중"이 진짜 요구사항인지부터 따져보는 게 좋다.

06. 특수 경로 — HTTP + mTLS origination

ℹ 이게 필요한 경우만

① 파트너 API가 client 인증서(mTLS) 를 요구하고, 그걸 앱마다가 아니라 게이트웨이 한 곳에서 중앙관리하고 싶을 때. ② 게이트웨이에서 HTTP 레벨 메트릭·로그가 꼭 필요할 때. 대가는 앱을 httpshttp로 바꿔야 함(침습적). 그 외엔 passthrough(주류 경로 섹션)가 낫다.

구조는 passthrough와 정반대다. 앱이 평문 HTTP를 보내면, 게이트웨이가 메시 mTLS 연결을 종단해서 L7 평문을 본 뒤(여기서 관측이 생김), 자기 client 인증서로 새 TLS 연결을 외부에 맺는다. TLS가 두 번, 별개 연결로 일어난다.

APP NODE App http :80 Sidecar Envoy EGRESS NODE Egress Gateway terminate to re-originate Partner API requires mTLS conn 1 mesh mTLS conn 2 mTLS (client cert)
그림 3. MUTUAL origination — conn①(메시 mTLS)과 conn②(client cert mTLS)는 게이트웨이에서 끊겼다 다시 맺는 별개 연결, 그 사이 L7 평문 노출.

conn①과 conn②는 게이트웨이에서 끊겼다 다시 맺어지는 별개 연결. 그 사이에 L7 평문이 노출되어 관측이 가능해진다. 게이트웨이가 client cert 보유(blast radius 주의).

설정 — passthrough와 다른 점

차이: ServiceEntry가 HTTP/HTTPS 포트를 갖고, VirtualService는 http:로 라우팅, DestinationRule이 두 개(메시 leg ISTIO_MUTUAL + 외부 leg MUTUAL origination), 그리고 client 인증서 secret이 필요하다.

# step 0 — client 인증서 secret (SDS)
# tls.crt/tls.key = 우리가 파트너에게 제시할 client 인증서
# ca.crt          = 파트너의 SERVER 인증서를 검증할 CA  (혼동 주의!)
kubectl create secret generic partner-client-cert -n istio-system \
  --from-file=tls.crt=client.pem \
  --from-file=tls.key=client.key \
  --from-file=ca.crt=partner-ca.pem
# ① ServiceEntry — 포트 80(HTTP)+443(HTTPS)
apiVersion: networking.istio.io/v1
kind: ServiceEntry
metadata: { name: partner-api, namespace: istio-system }
spec:
  hosts: [api.partner.example.com]
  ports:
    - { number: 80,  name: http,  protocol: HTTP }   # 앱이 평문으로 거는 포트
    - { number: 443, name: https, protocol: HTTPS }  # 게이트웨이가 originate 하는 포트
  resolution: DNS
  location: MESH_EXTERNAL
# ② Gateway — 메시 mTLS 종단 리스너
apiVersion: networking.istio.io/v1
kind: Gateway
metadata: { name: egress-partner, namespace: istio-system }
spec:
  selector: { istio: egressgateway }
  servers:
    - port: { number: 443, name: https, protocol: HTTPS }
      hosts: [api.partner.example.com]
      tls: { mode: ISTIO_MUTUAL }     # 사이드카→게이트웨이 leg 종단
# ③ DestinationRule ① — 사이드카→게이트웨이 (ISTIO_MUTUAL)
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata: { name: egressgateway-partner, namespace: istio-system }
spec:
  host: istio-egressgateway.istio-system.svc.cluster.local
  subsets:
    - name: partner
      trafficPolicy:
        portLevelSettings:
          - port: { number: 443 }
            tls: { mode: ISTIO_MUTUAL, sni: api.partner.example.com }
# ④ VirtualService — http 라우팅 2-leg
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata: { name: route-partner, namespace: istio-system }
spec:
  hosts: [api.partner.example.com]
  gateways: [mesh, egress-partner]
  http:
    - match: [{ gateways: [mesh], port: 80 }]
      route: [{ destination: { host: istio-egressgateway.istio-system.svc.cluster.local,
                               subset: partner, port: { number: 443 } } }]
    - match: [{ gateways: [egress-partner], port: 443 }]
      route: [{ destination: { host: api.partner.example.com, port: { number: 443 } } }]
# ⑤ DestinationRule ② — 게이트웨이→외부 (MUTUAL origination)
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata: { name: originate-mtls-partner, namespace: istio-system }
spec:
  host: api.partner.example.com
  trafficPolicy:
    portLevelSettings:
      - port: { number: 443 }
        tls:
          mode: MUTUAL                       # ← mTLS origination
          credentialName: partner-client-cert   # step 0 의 secret
          sni: api.partner.example.com
ℹ 검증 3종

istioctl proxy-config secret deploy/istio-egressgateway -n istio-system → 인증서가 SDS로 로드됐는지(cert 문제 80%가 여기). ② istioctl proxy-config cluster deploy/istio-egressgateway -n istio-system --fqdn api.partner.example.com -o json | grep -A5 transportSocket → MUTUAL/SNI 확인. ③ 앱에서 curl -v http://api.partner.example.com → 200 + 게이트웨이 access log에 출현.

ℹ What you might be missing
  • origination은 게이트웨이가 client 개인키를 보유하므로 게이트웨이 Pod 침해 = 파트너 자격증명 침해다 — 키를 앱에서 게이트웨이로 옮긴다는 건 blast radius를 옮긴다는 뜻이다.
  • 앱→게이트웨이 구간에 평문 HTTP가 존재하므로 메시 STRICT mTLS가 사실상 전제다(PERMISSIVE면 내부 평문 노출 구멍).
  • VirtualService gateways:Gateway CRD 이름을 정확히 — Deployment 이름이나 기본 istio-egressgateway Gateway를 잘못 적으면 leg 2가 조용히 무시된다.
  • SDS secret은 게이트웨이와 같은 네임스페이스(istio-system)에 있어야 origination DR이 읽는다.

07. TLS 모드 정밀 비교 (레퍼런스)

주류 경로와 특수 경로가 두 실무 경로였다면, 여기는 그 바탕에 깔린 TLS 모드 자체를 정리한다. 외부 구간 TLS를 다루는 방식은 본질적으로 세 가지다.

⚠ 전제 교정

"앱이 HTTPS로 직접 종단하면서 동시에 게이트웨이가 mTLS origination을 한다"는 같은 hop에서 성립할 수 없다. TLS는 point-to-point — 한 연결에서 종단은 한 번뿐이다. 앱이 외부와 직접 TLS를 맺으면(end-to-end HTTPS) 게이트웨이는 암호문만 흘리는 passthrough밖에 못 한다. 게이트웨이가 인증서를 새로 제시하려면(origination) 앱이 TLS를 맺지 않아야 하고(평문 HTTP) 게이트웨이가 TLS 클라이언트 종단이 돼야 한다.

모드 앱이 보내는 것 게이트웨이 역할
PASSTHROUGH (주류) HTTPS — 앱이 TLS 종단 복호화 안 함. SNI만 읽어 라우팅, 암호문 forward. L4만 보임
MUTUAL (mTLS origination, 특수) 평문 HTTP conn① 종단 → L7 가시 → conn② 신규 mTLS, client cert 제시 + server 검증
SIMPLE (TLS origination) 평문 HTTP MUTUAL과 같으나 client cert 없이 server cert만 검증(one-way)

모드별 게이트웨이 가시성

관측 항목 레이어 PASSTHROUGH MUTUAL SIMPLE
Source IP · Dest SNI L4
송수신 바이트 · 연결 시간 L4
HTTP method · path L7
status code (istio_requests_total) L7
request / response header L7
distributed tracing L7
게이트웨이가 client 인증서 제시(외부)
ℹ What you might be missing

세 모드는 호스트별로 섞어 쓸 수 있다 — 가시성이 필요한 파트너는 MUTUAL, end-to-end 암호화를 깨면 안 되는 컴플라이언스 파트너는 PASSTHROUGH로 DestinationRule을 따로 두면 된다. 하나로 통일할 이유가 없다. 또 auto_sni/auto_san_validation이 기본 활성이라, origination DR에 sni를 명시하지 않으면 downstream Host 헤더에서 SNI를 자동 추론하고 upstream 인증서를 그 호스트로 검증한다 — 의도와 다른 SNI가 나갈 수 있으니 origination에선 sni를 명시하는 게 안전하다.


08. 노드 핀닝과 가용성의 트레이드오프

강제 섹션의 ③ 네트워크 강제를 완성하려면 외부 NAT를 소수의 전용 노드에만 부여하고 게이트웨이 Pod를 그 노드로 끌어당겨야 한다. 세 가지를 함께 쓴다: nodeSelector(그 노드를 선택), taint/toleration(다른 워크로드는 못 들어오게 + 게이트웨이는 용인), podAntiAffinity(복제본을 서로 다른 노드로 분산).

# 전용 노드에 라벨 + 테인트
kubectl label node egress-1 egress-2 node-role=egress
kubectl taint node egress-1 egress-2 dedicated=egress:NoSchedule
# IstioOperator — egress 게이트웨이 배치
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
  components:
    egressGateways:
      - name: istio-egressgateway
        enabled: true
        k8s:
          replicaCount: 2
          nodeSelector:
            node-role: egress           # egress 노드로 끌어당김
          tolerations:
            - key: dedicated
              operator: Equal
              value: egress
              effect: NoSchedule         # 테인트 용인 → 그 노드에 뜰 수 있음
          affinity:
            podAntiAffinity:             # 복제본을 다른 노드로 분산
              requiredDuringSchedulingIgnoredDuringExecution:
                - labelSelector:
                    matchLabels:
                      istio: egressgateway
                  topologyKey: kubernetes.io/hostname
          podDisruptionBudget:
            minAvailable: 1              # 드레인/업그레이드 중 최소 1 유지

숨은 비용: 핀닝은 가용성을 깎는다

모든 외부 트래픽을 소수 노드로 모으는 순간, 그 노드들이 전체 외부 통신의 단일 장애점(SPOF) 이 된다. 핀닝을 강하게 할수록(노드 수 $N$을 줄일수록) 보안·IP 고정은 좋아지지만 가용성은 떨어진다.

egress 경로가 살아있을 확률은, 노드 장애가 독립이라 가정하면 "최소 한 노드라도 살아있을 확률"이다. 한 노드가 죽을 확률이 $(1-p)$이고 $N$개가 동시에 다 죽어야 경로가 끊기므로:

$$A_{\text{sys}} = 1 - (1-p)^{N}$$

곱 $(1-p)^N$은 "$N$개가 전부 죽는" 사건의 확률(독립이라 곱)이고, 경로가 살려면 그 여사건이면 되니 $1$에서 뺀다. 연간 다운타임은 가용성의 여집합에 1년(분)을 곱한 값:

$$D_{\text{year}} = (1 - A_{\text{sys}}) \times 525{,}600 \ \text{min}$$

노드 수 $N$ (노드당 $p=0.99$) $A_{\text{sys}}$ 연간 다운타임 nines
1 99.0000% 약 3.65 일/년 2.00
2 99.9900% 약 52.56 분/년 4.00
3 99.999900% 약 31.5 초/년 6.00

$N=1$로 내리면 가용성이 노드당 가용성과 정확히 같아지고(여분이 없으니), $N=2$로만 올려도 nine이 두 배 가까이 뛴다. 실무 결론: 전용 egress 노드는 최소 2개(가급적 3개, AZ 분산), minAvailable: 1 PDB, podAntiAffinity 필수. 노드 1개 핀닝은 "특정 노드로만"을 가장 엄격히 만족하지만 운영적으로는 금지에 가깝다.

ℹ What you might be missing

이 모델은 독립 장애를 가정한다. 현실에선 egress 노드들이 같은 ToR 스위치, 같은 NAT 게이트웨이, 같은 방화벽 정책을 공유하면 그 공통 요소가 진짜 SPOF다 — 노드를 3개로 늘려도 NAT가 하나면 $A_{\text{sys}}$ 계산은 거짓 위안이 된다. 둘째, 노드 장애만이 아니라 게이트웨이 재시작 시 connection draining도 다운타임의 큰 부분이다(자세한 drain/grace 타임라인·preStop·기본 5초 drain은 egress 운영 deep-dive 참조). 셋째, egress를 소수 노드로 모으면 그 노드의 conntrack 테이블·소스 포트 고갈(SNAT port exhaustion) 이 새 병목으로 등장한다 — 한 출구 IP당 약 64K 포트, 외부 목적지가 적으면 동시연결 한계에 먼저 부딪힌다.


09. 운영·장애 진단 — 무엇이 깨졌는지 읽기

egress 경로는 hop이 많아(앱 → 사이드카 → 게이트웨이 → 파트너), 장애 시 "어디서" 깨졌는지부터 좁혀야 한다. 세 질문이 렌즈다 — Routing(가야 할 곳으로 가는가), Transform(TLS·인증서·헤더가 의도대로 바뀌는가), Record(무슨 일이 기록에 남는가).

ℹ 이 문서 vs 운영 정본

본 문서는 설계·TLS 모델 정본이라 여기선 response_flags → 진단 명령 매핑(트래픽이 깨졌을 때 1차 단서 읽기)까지만 다룬다. 종료/drain 타임라인·grace tuning·롤링 업그레이드 중 무중단·모니터링 대시보드 구성 같은 운영 deep-dive는 egress 운영 정본에 위임한다.

Record — Envoy response flags가 1차 단서

flag 의미 egress에서의 전형적 원인
NR no route 오설정 1순위. VirtualService gateways/host/port/sniHosts 매칭 실패. passthrough에서 http:로 잘못 쓴 경우 포함
UH no healthy upstream route는 됐으나 endpoint 0. ServiceEntry host DNS 미해석, resolution 설정 문제
UF upstream conn failure TCP 연결 실패. 노드 NAT/라우트 부재, 방화벽 차단. config 밖(커널) 문제
UC upstream conn termination 붙었다 끊김. origination 핸드셰이크 실패(파트너가 cert reject) 또는 파트너 reset
NC no cluster route는 cluster를 가리키는데 부재 — subset/DR 누락

Routing — 미등록 트래픽은 어디로 가나

Envoy엔 두 합성 cluster가 있다. BlackHoleClusterREGISTRY_ONLY일 때 미등록 목적지가 보내지는 곳으로 즉시 실패하며 로그에 명시적으로 남는다("막혔다"가 보임). PassthroughCluster는 기본값 ALLOW_ANY일 때 미등록 목적지를 그대로 통과시키는 곳이라 — 게이트웨이를 안 거치고 조용히 나간다. "왜 이 트래픽만 게이트웨이를 안 거치지?"의 절반은 PassthroughCluster, 나머지 절반은 강제 섹션에서 본 excludeOutboundIPRanges 어노테이션이다.

질문 명령
공통 프록시가 컨트롤플레인과 sync 됐나 istioctl proxy-status
Routing listener→route→cluster→endpoint 체인 istioctl proxy-config listener\|route\|cluster\|endpoint deploy/istio-egressgateway -n istio-system
Transform 인증서가 게이트웨이에 로드됐나 istioctl proxy-config secret deploy/istio-egressgateway -n istio-system
Record 설정 정합성 정적 점검 istioctl analyze -n istio-system

증상 → 원인 → 진단 명령

# UF — Connection refused (TCP 연결 자체 실패, config 밖 문제)
istioctl proxy-config endpoint deploy/istio-egressgateway -n istio-system | grep partner
openssl s_client -connect api.partner.example.com:443 -servername api.partner.example.com
ip route get 203.0.113.10

# UC — TLS handshake failure (origination, 핸드셰이크 실패)
istioctl proxy-config secret deploy/istio-egressgateway -n istio-system
openssl s_client -connect api.partner.example.com:443 -servername api.partner.example.com -CAfile partner-ca.pem
kubectl -n istio-system get destinationrule originate-mtls-partner -o yaml

# 503 NR — no route (passthrough인데 http:로 작성, gateways 이름 mismatch, leg 누락)
istioctl proxy-config route deploy/istio-egressgateway -n istio-system
istioctl analyze -n istio-system

# 503 UH — no healthy upstream (DNS 미해석, resolution 오류)
istioctl proxy-config cluster deploy/istio-egressgateway -n istio-system --fqdn api.partner.example.com
kubectl -n istio-system get serviceentry partner-api -o yaml

# bypass — 게이트웨이 안 거치고 그냥 나감 (ALLOW_ANY / excludeOutboundIPRanges)
istioctl proxy-config cluster deploy/myapp -n <app-namespace> | grep -i passthrough
kubectl get pod -l app=myapp -n <app-namespace> -o jsonpath="{.items[*].metadata.annotations}"
ℹ What you might be missing
  • response_flags를 보려면 access log가 켜져 있어야 한다 — meshConfig.accessLogFile: /dev/stdout이 꺼져 있으면 1차 단서가 통째로 없다. passthrough는 TCP access log라 SNI·바이트·flags만 남는다.
  • istioctl proxy-configEnvoy가 컨트롤플레인에서 받은 설정을 보여줄 뿐 커널 소켓·conntrack 상태가 아니다 — config는 완벽한데 UF면 문제는 Envoy 밖(노드·방화벽)이고, egress 노드에서 openssl s_client·ss·conntrack으로 봐야 한다.
  • response flag는 철저히 Envoy 시점이다 — 우리 ca.crt가 틀려서 우리가 파트너 cert를 거부하면 그건 upstream이 끊은 게 아니라 우리쪽 핸드셰이크 에러로 나와서 방향을 헷갈린다.

10. 멘탈모델 정착 — 실무 매핑

마지막으로 anchor를 한 문장으로 되감으면: egress gateway는 외부행을 모으는 라우팅 수렴점일 뿐이고, "TLS를 누가 종단하나" 하나가 가시성·앱 변경·암호화·인증서 위치를 전부 결정하며, 진짜 강제는 게이트웨이가 아니라 그 아래 네트워크 3계층에서 나온다. 이 한 문장에서 설정·모니터링·핀닝·진단이 전부 따라 나온다. 아래는 그 모델을 실제 환경에 떨어뜨린 지도다.

개념 프로덕션 적용 홈랩 검증 (RTX 3090 / KVM)
주류: HTTPS + passthrough 앱 그대로 두고 ServiceEntry(TLS)+Gateway(PASSTHROUGH)+VS(sniHosts)로 파트너별 경로 강제, 출구 IP 고정 self-signed로 mock 파트너 HTTPS 서버 띄우고 passthrough 라우팅 + SNI 메트릭 관찰
DNS resolution 단일 호스트 DNS, air-gap 파트너는 STATIC+IP, 와일드카드는 NONE DNSSTATICNONE 전환하며 재해석 주기(기본 30s, dnsRefreshRate)·라우팅 차이를 config_dump로 관찰
모니터링 분리 게이트웨이 L4+SNI + 앱 OpenTelemetry로 L7 보완 istio_tcp_* vs 앱 메트릭 비교, passthrough L7 부재 확인
특수: mTLS origination 파트너 mTLS 필요 호스트만 origination, SDS+cert-manager 로테이션 mock 파트너에 client cert 요구 걸고 MUTUAL DR로 핸드셰이크 재현
3계층 강제 REGISTRY_ONLY + CNI egress NetworkPolicy drop + egress 노드만 NAT ALLOW_ANYREGISTRY_ONLY로 Passthrough vs BlackHole 로그 비교 (홈랩 CNI=Calico → GlobalNetworkPolicy egress drop, 노드 라우팅 한정으로 보강)
노드 핀닝 + 진단 전용 egress 노드 2~3개, response_flags 런북, proxy-config 체인 점검 의도적으로 route/cert 깨뜨리고 NR/UC/UH·flag 관찰

핵심 정리

What you might be missing


See also


ℹ 출처 (Istio 공식 문서, 검증 기준 Istio 1.30 / `networking.istio.io/v1`)
  • Egress Gateways (HTTPS/TLS passthrough 섹션: ServiceEntry protocol: TLS, Gateway tls.mode: PASSTHROUGH, VirtualService tls+sniHosts), Egress Gateways with TLS Origination(MUTUAL/credentialName/sni, SDS secret), DNS ProxyingUnderstanding DNS(resolution DNS/STATIC/NONE, 프록시 30초 주기 백그라운드 해석, ISTIO_META_DNS_AUTO_ALLOCATE 240.240.0.0/16) — istio.io/latest/docs.
  • 가로채기(bridging) 패턴 및 TLS 종단 후 L7 메트릭 미수집 함정 — Istio egress TLS 종단 운영 사례 및 관련 이슈.
  • ※ 최신 문서는 egress 예시를 Gateway API로 이전 중이나, 본 문서는 on-prem 표준인 classic CRD 패턴(Gateway+VirtualService+DestinationRule)을 사용함. 두 패턴 모두 1.30에서 유효함.

관련 파일 · 참조

실제 매니페스트(이 클러스터에서 사용) - 📎 gateway-egress.yaml · 📎 serviceentry-httpbin-ext.yaml - 📎 destinationrule-egress.yaml · 📎 virtualservice-egress.yaml - 📎 20-egress/README.md · 📎 traffic.sh · 📎 proxy-dump.sh

공식 문서 - ↗ Istio: Egress Gateways · ↗ Egress Gateway for HTTPS (SNI passthrough) · ↗ Egress TLS Origination

선행: Istio 1.30 Helm 재설치 런북

관련 검증Egress mTLS 리포트 — HTTPS over mTLS (ISTIO_MUTUAL) · 구조 정본 — CRD·장단점·활용·운영