---
type: note
tags: [istio, multicluster, eastwest-gateway, sni, mtls]
created: 2026-06-07
---
# east-west gateway는 목적지 클러스터를 SNI에 인코딩해, mTLS를 복호화하지 않고 암호화된 채로 원격 워크로드까지 프록시한다
> [!abstract]
> 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 신원](sec__note-mtls-spiffe-identity.html), [Cluster 해부](xds__src-cluster-anatomy.html)
---
## 1. 배경: 단일 클러스터 mTLS가 멀티클러스터에서 깨지는 두 지점
east-west gateway가 왜 존재하는지는, 그것이 없을 때 무엇이 깨지는지에서 나온다. 먼저 정상 상태부터.
**단일 클러스터**에서는 sidecar가 목적지 pod IP를 직접 알고, Envoy cluster가 그 endpoint로 mTLS를 맺는다. 워크로드 A의 SPIFFE 신원이 워크로드 B에 그대로 제시되고, B의 AuthorizationPolicy는 "A가 부른 게 맞다"를 그 신원으로 판정한다([SPIFFE/mTLS 신원](sec__note-mtls-spiffe-identity.html)). 이 그림에서 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 |
전체 흐름을 한 장에 담으면.
```mermaid
flowchart LR
subgraph C1["cluster1 / network1"]
A["workload A
sidecar"]
EW1["east-west GW
(AUTO_PASSTHROUGH)"]
end
subgraph C2["cluster2 / network2"]
EW2["east-west GW
(AUTO_PASSTHROUGH)"]
B["workload B
sidecar"]
end
A -- "mTLS, SNI=outbound_.8080._.B.ns.svc" --> EW2
EW2 -- "same encrypted bytes,
routed by SNI" --> B
EW1 -. "symmetric path
for C2 to C1 -> A" .-> A
```
> [!note]
> 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의 모양은 이렇다.
```text
# 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": "", "portValue": 15443 }
}}}]}
]}
}
```
이 한 객체에 멀티클러스터의 본질 두 개가 다 들어 있다.
1. **endpoint가 원격 pod가 아니라 east-west gateway IP:15443**이다. network 분리로 pod에 직접 못 가니, istiod가 endpoint를 게이트웨이로 치환해 내려준다. (cluster 구조 일반론은 [Cluster 해부](xds__src-cluster-anatomy.html).)
2. **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) |
> [!important]
> 포트 **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](gw__src-egress-http-vs-https.html)의 `protocol: TLS` + `tls_inspector`와 같은 계열 — **TLS를 풀지 않고 ClientHello의 SNI만 추출**한다.
```text
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 파싱도, 세션 키도 필요 없다.
```mermaid
sequenceDiagram
participant A as workload A sidecar (c1)
participant GW as east-west GW (c2, AUTO_PASSTHROUGH)
participant B as workload B (c2)
A->>GW: TLS ClientHello, SNI=outbound_.8080_._.B.ns.svc
Note over GW: tls_inspector reads SNI only
NO decryption
Note over GW: sni-dnat: SNI -> cluster
outbound|8080||B.ns.svc
GW->>B: forward SAME encrypted bytes (tcp_proxy)
Note over A,B: mTLS handshake completes A<->B end-to-end
SPIFFE identity of A presented to B
B-->>A: encrypted response (via GW)
```
결정적 결과: **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](xds__note-data-plane-sync-state.html), 가시성 범위 한정은 [sidecar scope](gw__note-sidecar-scope.html).)
---
## 4. 떴는지 확인 — SNI 인코딩과 endpoint 치환을 한 번에 본다
클러스터 간 호출이 동작한다면, 호출자 sidecar의 cluster에 §2.1의 두 흔적이 찍혀 있어야 한다. 한 명령으로 둘 다 본다.
```bash
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 분리가 정상일 때):
```json
{
"name": "outbound|8080||B.ns.svc.cluster.local",
"sni": "outbound_.8080_._.B.ns.svc.cluster.local",
"ep": { "address": "", "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 신원](sec__note-mtls-spiffe-identity.html)).
- **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이 이 설정을 함께 깔아준다.