east-west gateway는 목적지 클러스터를 SNI에 인코딩해, mTLS를 복호화하지 않고 암호화된 채로 원격 워크로드까지 프록시한다
east-west gateway는 mTLS를 풀지 않고 봉투 겉면(ClientHello의 SNI)에 적힌 목적지만 읽어 다음 hop으로 넘기는 L4 SNI 라우터다. 멀티클러스터(network 분리) 메시에서 한 클러스터의 sidecar가 다른 클러스터의 워크로드를 부를 때, sidecar는 목적지 식별자를 SNI 필드에 인코딩해 보내고, 게이트웨이는 그 SNI만 읽어(AUTO_PASSTHROUGH) 암호 바이트를 그대로 흘린다. 종단을 안 하므로 워크로드↔워크로드 mTLS가 관통해 보존된다. 이 문서는 그 메커니즘과 왜를 다룬다(운영 매니페스트는 위임).
대상환경: Istio 1.30, multi-network 멀티클러스터 · 대상독자: 멀티클러스터 mTLS 신원 보존이 왜·어떻게 동작하는지 알고 싶은 SRE · 선행개념: SPIFFE/mTLS 신원, Cluster 해부
1. 배경: 단일 클러스터 mTLS가 멀티클러스터에서 깨지는 두 지점
east-west gateway가 왜 존재하는지는, 그것이 없을 때 무엇이 깨지는지에서 나온다. 먼저 정상 상태부터.
단일 클러스터에서는 sidecar가 목적지 pod IP를 직접 알고, Envoy cluster가 그 endpoint로 mTLS를 맺는다. 워크로드 A의 SPIFFE 신원이 워크로드 B에 그대로 제시되고, B의 AuthorizationPolicy는 "A가 부른 게 맞다"를 그 신원으로 판정한다(SPIFFE/mTLS 신원). 이 그림에서 sidecar↔sidecar는 1-hop이고, 중간에 끼어드는 자가 없다.
멀티클러스터 — 특히 network가 분리된 경우(클러스터 간 pod CIDR이 서로 라우팅 불가) — 에서는 이 그림이 두 군데서 깨진다.
- 연결성: cluster1의 sidecar가 cluster2의 pod IP로 직접 TCP를 못 연다. 두 network는 L3에서 서로의 pod CIDR을 모른다.
- 신원 보존: 누군가 두 클러스터 사이에서 TLS를 종단(복호화)하면, 그 지점에서 peer 신원이 그 중간자의 신원으로 바뀐다. B가 보는 건 더 이상 A가 아니다. end-to-end SPIFFE가 끊긴다.
순진한 해법(클러스터 경계에 보통 게이트웨이를 세워 TLS 종단 후 재암호화)은 연결성은 풀지만 신원을 죽인다. 신원을 살리려면 중간 게이트웨이가 절대 TLS를 풀지 않아야 한다. 그런데 풀지 않으면 게이트웨이는 안을 못 보는데, 어떻게 "어디로 보낼지"를 정하나? 이 긴장이 east-west gateway 설계 전체를 결정한다.
2. 핵심 아키텍처: 봉투 겉면(SNI)에 목적지를 적어 라우터에게 읽힌다
머릿속에 담을 한 장면(anchor): 게이트웨이가 편지 안을 못 열게 하려면, 받는 사람을 봉투 겉면에 적으면 된다. TLS에서 그 겉면이 평문으로 노출되는 유일한 칸이 ClientHello의 SNI다. 그래서 sidecar는 목적지 식별자를 SNI에 인코딩해 보내고, 게이트웨이는 그 한 줄만 읽어 라우팅한다 — 복호화 0.
이 한 문장에서 나머지 모든 디테일이 따라 나온다. 메커니즘을 세 부품으로 분해하면.
| 부품 | 그게 답하는 질문 | 어떻게 |
|---|---|---|
| SNI 인코딩 (sidecar 쪽) | "게이트웨이가 안을 못 보는데 목적지를 어떻게 전달하나?" | 목적지를 outbound_.PORT_.SUBSET_.FQDN로 SNI에 적음 |
| AUTO_PASSTHROUGH (게이트웨이 listener) | "게이트웨이가 어떻게 복호화 없이 SNI만 읽나?" | tls_inspector로 ClientHello의 SNI만 추출, TLS는 안 풂 |
| sni-dnat (게이트웨이 라우팅) | "추출한 SNI를 어디로 보내나?" | SNI를 동일 이름 내부 cluster로 역매핑해 tcp_proxy |
전체 흐름을 한 장에 담으면.
A의 sidecar는 cluster2의 east-west GW(EW2)로 곧장 mTLS를 맺는다 — 자기 클러스터 EW1을 거치지 않는다. EW1은 반대 방향(cluster2 → cluster1의 워크로드) 트래픽의 진입점이다. 게이트웨이는 network마다 하나 있는 양방향 관문이고, 트래픽은 항상 목적지 network의 게이트웨이로 들어간다.
2.1 부품 ①: SNI 인코딩 — sidecar가 목적지를 봉투에 적는다
평범한 Envoy cluster의 transport socket은 TLS를 맺을 때 SNI를 목적지 hostname(또는 비움)으로 채운다 — "어떤 server 인증서를 줄까"용이다. east-west 경로용 cluster는 이 SNI를 목적지 식별자 문자열로 덮어쓴다. istiod가 멀티클러스터 sidecar에 내려주는 cluster의 모양은 이렇다.
# istioctl proxy-config cluster deploy/workload-a --fqdn B.ns.svc.cluster.local -o json 의 발췌
{
"name": "outbound|8080||B.ns.svc.cluster.local",
"transportSocket": {
"name": "envoy.transport_sockets.tls",
"typedConfig": {
"sni": "outbound_.8080_._.B.ns.svc.cluster.local" # ← 목적지를 SNI에 인코딩
}
},
"loadAssignment": { "endpoints": [
{ "lbEndpoints": [ { "endpoint": { "address": {
"socketAddress": { "address": "<EW2 gateway IP>", "portValue": 15443 }
}}}]}
]}
}
이 한 객체에 멀티클러스터의 본질 두 개가 다 들어 있다.
- endpoint가 원격 pod가 아니라 east-west gateway IP:15443이다. network 분리로 pod에 직접 못 가니, istiod가 endpoint를 게이트웨이로 치환해 내려준다. (cluster 구조 일반론은 Cluster 해부.)
- SNI가 hostname이 아니라
outbound_.{port}_.{subset}_.{fqdn}인코딩 문자열이다. 일반 TLS의 SNI는 인증서 선택용이지만, 여기 SNI는 "원격 게이트웨이가 어느 내부 cluster로 보낼까"의 라우팅 키다. cluster 이름outbound|8080||B...을_로 평탄화한 게 SNI라는 점을 보면, 둘이 같은 식별자임을 알 수 있다 — 이게 뒤의 역매핑을 공짜로 만든다.
| 구분 | 일반 cluster의 SNI | east-west용 cluster의 SNI |
|---|---|---|
| 값 | 목적지 hostname 또는 없음 | outbound_.PORT_.SUBSET_.FQDN |
| 용도 | server 인증서 선택(SAN 매칭) | 원격 게이트웨이의 라우팅 키 |
| endpoint | 실제 목적지 IP | east-west gateway IP:15443 |
| 누가 읽나 | 목적지 워크로드 | east-west gateway(passthrough) |
포트 15443은 east-west gateway의 TLS auto-passthrough listener다. 메시 내부 inbound(15006)·outbound(15001)와 별개의, 멀티클러스터 전용 포트다.
2.2 부품 ②③: AUTO_PASSTHROUGH로 SNI만 읽고, sni-dnat으로 내부 cluster에 꽂는다
원격 east-west gateway의 listener는 tls.mode: AUTO_PASSTHROUGH로 떠 있다. 동작은 egress passthrough의 protocol: TLS + tls_inspector와 같은 계열 — TLS를 풀지 않고 ClientHello의 SNI만 추출한다.
protocol: TLS + AUTO_PASSTHROUGH → listener:[ tls_inspector → tcp_proxy ]
ClientHello의 SNI 추출 → SNI 문자열을 cluster 이름으로 해석 → 그 cluster로 tcp_proxy
복호화 0, L7 0 (암호 바이트 그대로 통과)
게이트웨이가 하는 일은 SNI 디코딩 → 매칭되는 내부 cluster 선택뿐이다.
받은 SNI: outbound_.8080_._.B.ns.svc.cluster.local
│ │ │ └── fqdn → 어느 서비스
│ │ └────── subset → 어느 DestinationRule subset
│ └─────────── port → 어느 포트
└──────────────────── direction(outbound)
▼ 이 인코딩을 cluster "outbound|8080||B.ns.svc.cluster.local" 로 역매핑
→ 그 cluster의 endpoint(= cluster2 내부 B의 실제 pod들)로 tcp_proxy
이 "SNI를 보고 동일 이름의 내부 cluster로 dnat"이 sni-dnat 라우터 모드다. istiod가 멀티클러스터 게이트웨이에 ROUTER_MODE=sni-dnat을 켜면, 게이트웨이는 각 서비스마다 outbound_.* SNI에 매칭되는 passthrough cluster를 자동 생성한다. 게이트웨이는 받은 암호 바이트를 건드리지 않고 그 내부 cluster의 실제 pod endpoint로 tcp_proxy할 뿐이다. SNI 문자열과 cluster 이름이 같은 식별자(§2.1)이므로 이 역매핑은 문자열 변환 한 번이다 — L7 파싱도, 세션 키도 필요 없다.
결정적 결과: TLS 핸드셰이크는 A와 B 사이에서 완성된다. 게이트웨이는 그 핸드셰이크의 ClientHello만 엿보고 바이트를 옮겼을 뿐, 세션 키를 모른다. 따라서 B는 A의 진짜 SPIFFE 신원을 받고, AuthorizationPolicy가 그 신원으로 정상 평가된다 — 게이트웨이가 신원을 "삼키지" 않는다.
2.3 왜 종단하면 안 되나 — 대안과의 대조로 설계를 못 박기
AUTO_PASSTHROUGH의 당위는 그 반대를 그려보면 확정된다. 만약 east-west gateway가 ISTIO_MUTUAL/SIMPLE로 TLS를 종단한다면.
- 게이트웨이가 A의 mTLS를 풀고 → 자기 신원으로 B에 새 mTLS를 맺는다(2-hop).
- B가 보는 peer 신원은 east-west gateway의 SPIFFE가 된다 — A의 신원이 사라진다.
- AuthorizationPolicy
from.source.principals: [A]가 깨진다(게이트웨이 신원만 보임). - 게이트웨이 내부 평문 구간 노출 + 복호화 비용 발생.
AUTO_PASSTHROUGH는 이 모두를 피한다 — L4 SNI 라우터로만 동작하므로 신원이 게이트웨이를 투명하게 관통한다. 이것이 ingress gateway와 갈리는 본질이다. ingress는 외부→메시 진입이라 종단·L7 라우팅이 목적이지만, east-west는 메시 내부 mTLS를 그대로 다른 network로 운반하는 게 목적이다.
| 구분 | ingress gateway | east-west gateway |
|---|---|---|
| TLS 처리 | 종단(복호화), L7 라우팅 | AUTO_PASSTHROUGH, SNI만 |
| 신원 | 클라이언트→GW에서 끝, GW가 새 신원으로 mesh 진입 | A→B end-to-end 보존 |
| 라우팅 키 | host/path(L7) | SNI 인코딩 문자열(L4) |
| 포트 | 80/443 등 | 15443 |
| 목적 | 외부 트래픽 진입 | network 간 mTLS 운반 |
3. 무엇이 이 라우팅을 켜고 끄나 — network 식별자가 방아쇠
위 메커니즘은 istiod가 어떤 endpoint가 어느 network에 있는지 알 때만 성립한다. 그 지식은 세 식별자에서 온다.
| 식별자 | 어디서 정의 | 역할 |
|---|---|---|
| meshID | install values (global.meshID) |
여러 클러스터를 하나의 신뢰·메시 단위로 묶음. 같은 meshID + 공유 root CA여야 mTLS 신뢰가 클러스터를 횡단 |
| clusterName | install values (global.multiCluster.clusterName) |
각 클러스터에 고유 이름 부여. endpoint 출처 식별·메트릭 라벨(source_cluster)에 쓰임 |
| network | namespace label topology.istio.io/network + gateway 설정 |
endpoint가 어느 network에 속하는지. 이게 핵심 스위치 |
istiod의 endpoint discovery(EDS) 분기는 단순하다 — 이 한 비교가 §2 전체를 켤지 말지 결정한다.
목적지 endpoint의 network == 호출자 sidecar의 network ?
├─ 같다 → endpoint를 pod IP 그대로 내려줌(직접 연결, east-west GW 불필요)
└─ 다르다 → endpoint를 그 network의 east-west GW IP:15443으로 치환 + SNI 인코딩
즉 east-west gateway 경유는 network가 다를 때만 일어난다. 같은 network면(예: flat pod network를 공유하는 멀티클러스터) sidecar가 원격 pod로 직접 mTLS를 맺고 게이트웨이를 안 거친다. 트리거는 클러스터 경계가 아니라 network 경계다. (어떤 endpoint가 sidecar에 보이는가의 일반 메커니즘은 data-plane sync state, 가시성 범위 한정은 sidecar scope.)
4. 떴는지 확인 — SNI 인코딩과 endpoint 치환을 한 번에 본다
클러스터 간 호출이 동작한다면, 호출자 sidecar의 cluster에 §2.1의 두 흔적이 찍혀 있어야 한다. 한 명령으로 둘 다 본다.
istioctl proxy-config cluster deploy/workload-a \
--fqdn B.ns.svc.cluster.local -o json \
| jq '.[0] | {name, sni: .transportSocket.typedConfig.sni,
ep: .loadAssignment.endpoints[0].lbEndpoints[0].endpoint.address.socketAddress}'
기대 출력(멀티클러스터·network 분리가 정상일 때):
{
"name": "outbound|8080||B.ns.svc.cluster.local",
"sni": "outbound_.8080_._.B.ns.svc.cluster.local",
"ep": { "address": "<EW2 gateway IP>", "portValue": 15443 }
}
판정 기준 두 줄:
- sni가 outbound_.* 인코딩이면 ✓ — SNI 라우팅용 cluster가 맞다. 평범한 hostname이면 멀티클러스터 EDS가 안 걸린 것.
- ep.address가 원격 GW IP, portValue가 15443이면 ✓ — endpoint 치환 성공. 여전히 원격 pod IP면 network label(topology.istio.io/network) 미설정이다.
신원이 정말 보존됐는지는 도착지에서 본다: B의 AuthorizationPolicy 로그/메트릭에서 peer principal이 A의 SPIFFE(게이트웨이가 아니라)로 찍히면 end-to-end가 살아있다는 증거다.
정리
한 문장 멘탈 모델: east-west gateway는 편지를 열지 않는다 — sidecar가 봉투 겉면(SNI)에 적은 목적지만 읽어 암호 바이트를 그대로 다음 hop으로 넘기는 L4 라우터이고, 그래서 A↔B mTLS 신원이 게이트웨이를 투명하게 관통한다.
핵심 정리
- east-west gateway는 멀티클러스터(network 분리 시)의 클러스터 간 진입점이며, mTLS를 절대 종단하지 않는 L4 SNI 라우터다.
- sidecar는 목적지를 SNI에
outbound_.PORT_.SUBSET_.FQDN으로 인코딩하고, endpoint는 원격 east-west gateway IP:15443으로 치환된다. SNI 문자열 = cluster 이름의 평탄화 — 그래서 게이트웨이의 역매핑이 문자열 변환 한 번이다. - 게이트웨이는
AUTO_PASSTHROUGH(= tls_inspector로 SNI만 추출) + sni-dnat(ROUTER_MODE=sni-dnat)로 그 SNI를 동일 이름 내부 cluster로 역매핑해 암호 바이트를 그대로 tcp_proxy한다. - TLS 핸드셰이크는 A↔B 사이에서 완성 → A의 SPIFFE 신원이 B까지 보존 → AuthorizationPolicy 정상 평가.
- 분기 스위치는 network 식별자(
topology.istio.io/network): 같으면 직접 연결, 다르면 east-west GW 경유. meshID(신뢰 단위)·clusterName(출처)이 이를 뒷받침.
What you might be missing
- east-west GW 경유는 network 분리 시에만. 같은 flat network를 공유하는 멀티클러스터(primary-remote with shared network)는 sidecar가 원격 pod로 직접 가고 게이트웨이를 안 거친다. "멀티클러스터면 무조건 east-west GW"가 아니다 — 트리거는 클러스터 경계가 아니라 network 경계다.
- 공유 root CA가 전제. SNI 인코딩·passthrough가 다 동작해도, 두 클러스터가 같은 trust domain의 공유 root CA를 안 쓰면 A의 SVID를 B가 검증 못 해 핸드셰이크가 깨진다. east-west 라우팅은 신뢰 체인을 운반할 뿐 생성하지 않는다(SPIFFE/mTLS 신원).
- passthrough라 L7 관측이 0. 게이트웨이가 복호화를 안 하므로 클러스터 간 트래픽은 게이트웨이에서
istio_tcp_*(L4)만 잡히고 method/path/status(istio_requests_total)는 출발지/도착지 sidecar에서만 보인다. 멀티클러스터 트래픽을 게이트웨이 메트릭으로 L7 분석하려 하면 빈손이다. - SNI 인코딩 디버깅. 클러스터 간 호출이 안 되면 §4의 cluster dump로 (1) endpoint가 원격 GW IP:15443인지, (2)
transportSocket.sni가outbound_.*형태인지 먼저 확인하라. endpoint가 여전히 원격 pod IP면 network label 미설정, SNI가 평범한 hostname이면 멀티클러스터 EDS가 안 걸린 것이다. - ROUTER_MODE. east-west gateway가 일반 게이트웨이와 다른 결정적 점은
ISTIO_META_ROUTER_MODE=sni-dnat환경변수다. 평범한 ingress용 게이트웨이 deployment를 그대로 east-west로 쓰면 sni-dnat cluster가 안 생겨 SNI 라우팅이 동작하지 않는다.istioctl x create-remote-secret/멀티클러스터 install이 이 설정을 함께 깔아준다.