이중 mTLS Egress — 홉마다 DR 한 벌, 커넥션 설정은 그 홉을 여는 프록시에 박힌다
이중 mTLS = egress 경로의 두 홉이 각각 별개의 mTLS로 감싸이는 패턴이다.
홉 1(앱 sidecar→egressgateway)은 메시 mTLS(ISTIO_MUTUAL), 홉 2(egressgateway→외부)는
gateway가 사용자 인증서로 시작하는 mTLS(MUTUAL + credentialName). 머릿속에 넣을 한 문장:
DestinationRule은 홉마다 한 벌씩 필요하고, 각 벌의 connectionPool은 “그 홉의 연결을 여는
프록시"의 upstream cluster에 컴파일된다 — 홉 1 설정은 호출자 sidecar에, 홉 2 설정은 gateway에.
이 문서는 각 홉의 DR을 해부하고, 커넥션 설정이 어느 Envoy에서 발효되는지, 두 DR이 싸우는
고전 함정과 튜닝 도출식까지 정리한다. DR 일반론은
DestinationRule 만들기,
값 도출은 Egress TCP 처방전이 정본.
대상 환경: Istio 1.30.0, sidecar mesh, Helm gateway chart (homelab k8s 1.30.6 기준 컨벤션) 선행 개념: egress 2-홉 라우팅(HTTPS passthrough 가이드), DR 병합 규칙(DR 정본 §05)
1. 이중 mTLS의 정체 — 두 홉, 두 mTLS, 두 DR
먼저 용어 정리. 이 문서의 “이중 mTLS"는 HTTPS over mTLS와 다른 패턴이다.
| 패턴 | inner/leg-2 TLS의 주체 | gateway가 L7을 보나 | 외부 client cert |
|---|---|---|---|
| HTTPS over mTLS | 앱이 만든 TLS를 gateway가 안 풀고 통과 | ✗ (이중 봉투, L4만) | 앱이 직접 관리 |
| 이중 mTLS (본 문서) | gateway가 새 mTLS를 시작 (origination) | ✓ (홉 1 종단 후 평문 L7) | gateway가 중앙 관리 |
이중 mTLS에서 앱은 평문 http://로 호출한다. 요청의 암호화는 인프라가 두 번 입힌다:
- 홉 1 (sidecar → egressgateway): sidecar가 요청을 메시 mTLS(
ISTIO_MUTUAL)로 래핑. gateway는 이걸 종단하며 호출 워크로드의 SPIFFE 신원을 검증한다 — “누가 나가는가"의 확정 지점. - 홉 2 (egressgateway → 외부): gateway가 외부 서버가 요구하는 client cert를 제시하며 새 mTLS를 시작(
MUTUAL). 파트너사 상호 TLS 요건을 gateway 한 곳의 인증서로 충족한다 — “무엇을 제시하는가"의 중앙화 지점.
flowchart LR
subgraph mesh["ns: mesh-test"]
APP["app (curl http://...)"] --> SC["sidecar Envoy"]
end
subgraph sys["ns: istio-system"]
GW["istio-egressgateway"]
end
EXT["api.partner.example.com:443"]
SC -->|"hop1: ISTIO_MUTUAL<br/>(mesh cert, SPIFFE)<br/>DR-hop1 = sidecar cluster"| GW
GW -->|"hop2: MUTUAL<br/>(client cert via SDS)<br/>DR-hop2 = gateway cluster"| EXT두 홉은 암호화 컨텍스트가 완전히 독립이다 — 인증서 체계도(메시 CA vs 파트너 CA), 검증 대상도(호출자 SPIFFE vs 외부 서버 SAN), 설정 리소스도 다르다. 그래서 DR이 반드시 두 벌이고, 이 문서 전체가 그 두 벌의 해부다.
| DR-hop1 | DR-hop2 | |
|---|---|---|
host |
istio-egressgateway.istio-system.svc.cluster.local (gateway Service FQDN) |
api.partner.example.com (ServiceEntry host) |
tls.mode |
ISTIO_MUTUAL (메시 인증서 자동) |
MUTUAL + credentialName (사용자 인증서) |
| 검증 대상 | (Gateway 쪽이) 호출자 SPIFFE | 외부 서버 cert — sni + subjectAltNames |
connectionPool 발효 위치 |
호출자 sidecar의 upstream cluster | gateway의 upstream cluster |
| 배치 | 클라이언트 ns 또는 istio-system | gateway와 같은 ns (credentialName 제약) |
2. 홉 1 — sidecar → egressgateway (ISTIO_MUTUAL)
2.1 짝이 맞아야 하는 두 리소스
홉 1 mTLS는 DR(클라이언트 쪽)과 Gateway(서버 쪽)가 짝을 이뤄야 성립한다:
- DR-hop1
tls.mode: ISTIO_MUTUAL— sidecar가 자기 SPIFFE client cert를 제시하며 메시 mTLS를 만든다. 인증서·키·CA는 istiod SDS가 자동 공급하므로 경로/secret 지정이 없다. - Gateway server
tls.mode: ISTIO_MUTUAL— gateway listener가 client cert를 강제(requireClientCertificate)하고 mesh CA로 SPIFFE를 검증한 뒤 종단한다. 한쪽만 걸면 즉시 깨진다: gateway만 ISTIO_MUTUAL이고 DR이 없으면 sidecar가 평문을 보내 handshake가 거부된다(신원 통제 가이드 §6 함정 표).
sni와 subjectAltNames의 역할:
tls.sni— sidecar가 만드는 outer ClientHello의 SNI에 원본 목적지 호스트를 적재한다. gateway listener가 filter chain(어느 server/채널인가)을 고르는 매칭 키이므로, DRsni== Gatewayhosts[]== VS 매칭 호스트 3자 정렬이 깨지면 매칭 실패로 연결이 리셋된다.subjectAltNames— 이 홉에선 “sidecar가 검증하는 gateway의 신원"이다.ISTIO_MUTUAL은 검증 컨텍스트를 메시가 자동 구성하므로 보통 생략하고, 명시하면 gateway 서버 cert의 SPIFFE SAN(spiffe://<trust-domain>/ns/istio-system/sa/<gw-sa>)을 핀 고정하는 용도가 된다.
2.2 이 홉의 connectionPool은 어디서 발효되나
DR-hop1의 host가 gateway Service이므로, 이 trafficPolicy는 그 host로 연결을 여는 모든 클라이언트 sidecar의 upstream cluster(outbound|443|partner|istio-egressgateway.istio-system.svc.cluster.local)에 컴파일된다. 즉:
tcp.connectTimeout/tcp.maxConnections/tcp.tcpKeepalive— sidecar→gateway TCP 연결에 적용. 한도는 Envoy 프록시별 독립 집행이라 “클라이언트 pod당” 상한이다. pod 하나가 gateway로 수천 연결을 열 일은 드물어 보통 여유가 있지만, 외부 host DR만 튜닝하면 이 홉의 기본 한도가 숨은 병목이 될 수 있다(TCP 처방전 P1의 레이어 2).http.*(idleTimeout, maxRequestsPerConnection 등) — 이 패턴은 홉 1이 HTTP 라우트(gateway가 종단하므로 L7)라 http 설정도 유효하다. sidecar→gateway 커넥션 풀의 재사용·유휴 정리를 제어한다.
검증은 DR 정본 §08 절차 그대로 — 단 sidecar pod에서 본다:
istioctl proxy-config cluster deploy/sleep -n mesh-test \
--fqdn istio-egressgateway.istio-system.svc.cluster.local -o json | \
jq '.[] | {name, dr: .metadata.filterMetadata.istio.config,
max: .circuitBreakers.thresholds[0].maxConnections,
ka: .upstreamConnectionOptions.tcpKeepalive}'
3. 홉 2 — egressgateway → 외부 (MUTUAL + credentialName)
3.1 client cert 공급 두 방식 — SDS(credentialName) vs 파일 마운트
| 방식 | 선언 | 인증서 전달 경로 | 판단 |
|---|---|---|---|
| SDS (권장) | credentialName: <secret명> |
k8s Secret → istiod SDS → gateway Envoy (파드 재시작 불필요, 로테이션 자동 감지) | 표준. 단 gateway 전용 — sidecar에는 적용 불가 |
| 파일 마운트 (legacy) | clientCertificate: / privateKey: / caCertificates: (파일 경로) |
secret을 gateway pod에 volume mount, 경로 하드코딩 | 배포와 결합, 로테이션 시 재기동 — 신규 구성엔 비권장 |
credentialName의 두 가지 제약이 설계를 결정한다 (DR reference):
- gateway 워크로드 전용이다 — sidecar는 이 필드를 쓸 수 없다(파일 경로 방식만 가능). “외부 mTLS의 client cert는 gateway에서 중앙 관리"라는 이 패턴의 구도가 여기서 강제된다.
- secret은 gateway가 있는 namespace에 있어야 SDS가 찾는다. egressgateway가 istio-system이면 secret도 istio-system.
- gateway pod의 ServiceAccount에 secret 읽기 RBAC이 있어야 istiod가 credentialName을 서빙한다 — istiod는 SDS 요청을 SubjectAccessReview로 인가하므로, gateway SA에 해당 ns의
secretsget/watch/listRole이 없으면 503(UF,TLS_error: Secret is not supplied by SDS) + istiod 로그not authorized to read secrets로 실패한다. 표준 istio-egressgateway는 Helm chart가 이 Role을 깔아주지만, 주입 템플릿으로 만든 전용 gateway는 직접 추가해야 한다(T90 실측 — files/verify/T90/manifest.yaml의dmtls-egress-sdsRole 참고).
# 파트너가 발급/승인한 client cert + key + 서버 검증용 CA를 하나의 generic secret으로
kubectl create secret -n istio-system generic partner-client-credential \
--from-file=tls.key=client.partner.example.com.key \
--from-file=tls.crt=client.partner.example.com.crt \
--from-file=ca.crt=partner-ca.crt
# (선택) CRL을 함께 실으려면 --from-file=ca.crl=partner-ca.crl
키 이름은 tls.key/tls.crt/ca.crt 고정이다. ca.crt가 홉 2에서 외부 서버 cert를 검증하는 CA가 된다.
3.2 sni · subjectAltNames — 외부 서버를 검증하는 두 다이얼
홉 2의 검증 방향은 홉 1과 반대다 — 이번엔 gateway가 클라이언트로서 외부 서버를 검증한다:
sni— 외부 서버로 보내는 ClientHello의 SNI. 외부가 SNI 기반 vhost/LB라면 필수이고, 생략 시 서버가 default cert를 줘 SAN 검증이 어긋날 수 있다.subjectAltNames— 서버 cert의 SAN이 이 목록과 일치해야 통과. DNS 하이재킹·잘못된 라우팅으로 엉뚱한 서버에 client cert를 제시하는 사고를 막는 마지막 잠금이므로 프로덕션에선 명시를 권장한다.
3.3 DR-hop2의 배치와 스코프
DR 선택은 클라이언트 프록시 관점에서 일어난다: 클라이언트 ns의 DR > 서비스 ns의 DR > 루트 ns(istio-system)의 DR (DR 정본 §06). 홉 2의 “클라이언트"는 gateway pod이므로:
- DR-hop2는 gateway namespace(istio-system)에 배치한다 — credentialName의 secret 위치 제약과도 일치해 공식 task의 배치가 이것이다.
- istio-system은 루트 ns라 기본(
exportTo미지정 =*)으로는 mesh 전체에 보인다. sidecar가 이 DR을 집어 외부 host cluster에MUTUAL+credentialName을 입히려다 실패하는 노이즈를 막으려면exportTo: ["."]로 gateway ns 안으로 좁히는 것이 위생적이다(트래픽은 어차피 VS가 gateway로 우회시키므로 기능 손실 없음).
3.4 portLevelSettings — 한 host의 포트마다 다른 TLS 컨텍스트
한 DR host가 여러 포트를 덮을 때(예: 443은 파트너 mTLS, 8443은 단방향 TLS 테스트 엔드포인트) portLevelSettings가 TLS 컨텍스트를 포트 단위로 분리한다:
trafficPolicy:
portLevelSettings:
- port: { number: 443 }
tls: { mode: MUTUAL, credentialName: partner-client-credential, sni: api.partner.example.com }
connectionPool: { tcp: { maxConnections: 4096, connectTimeout: 3s } }
- port: { number: 8443 }
tls: { mode: SIMPLE, sni: sandbox.partner.example.com }
connectionPool: { tcp: { maxConnections: 128, connectTimeout: 3s } }
portLevelSettings는 상속이 아니라 통째 교체다. 포트 엔트리에 tls만 적으면 그 포트 cluster에서
top-level connectionPool이 사라진다(기본값으로 회귀). 트래픽이 실제 타는 포트 엔트리에
필요한 필드를 전부 재기재하는 것이 규칙 — subset을 쓰면 top→subset→port 3층에서 같은 교체가
두 번 일어난다(DR 정본 §05의 실측 2단 함정).
3.5 이 홉의 connectionPool은 gateway에서 발효된다
DR-hop2의 trafficPolicy는 외부 host의 upstream cluster(outbound|443||api.partner.example.com)에 컴파일되는데, 이 cluster로 실제 연결을 여는 프록시는 gateway다. 결과적으로:
- 한도(
maxConnections등)는 gateway Envoy에서 집행된다. sidecar 시절엔 pod마다 분산되던 연결이 gateway로 전사 합류하므로, DR 없이 방치하면 Envoy 기본 상한(1,024)에 gateway가 가장 먼저 부딪힌다 — access log flagUO,upstream_cx_overflow증가(TCP 처방전 P1). - 검증도 gateway pod에서 한다:
istioctl proxy-config cluster deploy/istio-egressgateway -n istio-system --fqdn api.partner.example.com .... 확인할 pod를 홉과 어긋나게 잡으면(레이어 1 설정을 sidecar에서 찾는 등) 영원히 “미적용"으로 보인다(keepalive 노트 §04). - 인증서·TLS 컨텍스트 변경은 기존 연결에 소급되지 않는다 — 이미 세워진 커넥션은 옛 client cert로 계속 산다. 수명 상한(
maxConnectionDuration)이 로테이션 반영 시간을 유계로 만든다(§6).
4. 고전 함정 — 두 DR이 같은 host를 두고 싸울 때
이 패턴의 장애 대부분은 “DR 두 벌"이라는 구조 자체에서 나온다.
함정 1 — 한 host에 DR 여러 벌. 홉 1 정책과 홉 2 정책을 실수로 같은 host에 갈라 쓰면(예: 외부 host에 DR 2벌) 에러가 아니라 조용한 병합이 일어난다: subsets는 합집합, top-level trafficPolicy는 생성시각이 가장 오래된 DR이 승자, 나머지는 무시. “왜 내 tls 설정이 안 먹지"의 단골 원인이다. 원칙: host당 DR 1벌 — 홉 1 DR은 gateway Service FQDN을, 홉 2 DR은 ServiceEntry의 외부 host를 host로 가지므로 애초에 겹칠 일이 없게 설계된다(DR 정본 §06, T54 실측).
함정 2 — host 문자열의 조용한 불일치. DR host는 서비스 레지스트리 문자열과 매칭되며 실패해도 에러가 없다. short-name(istio-egressgateway)은 DR namespace 기준으로 확장돼 존재하지 않는 host가 되고, 외부 host 오타는 ServiceEntry와 어긋나 그대로 무시된다. 증상은 “설정했는데 기본값” — proxy-config cluster의 dr: null이 시그니처다.
함정 3 — 홉 1 매칭 실패는 조용한 우회로 끝난다. VS mesh 매칭(gateways: [mesh], host/포트)이 어긋나면 트래픽은 gateway로 꺾이지 않고, ALLOW_ANY에선 sidecar가 직접 외부로 나가버린다 — ServiceEntry가 그 포트를 등록하지 않았으면 PassthroughCluster로, 등록했으면 SE의 outbound cluster(outbound|80||api.partner...)로 직행한다(T90 실측: 후자도 gateway access log 0건, mTLS 0겹으로 동일). 호출은 200이라 겉으론 멀쩡하지만 이중 mTLS는 한 겹도 적용되지 않은 상태다(client cert 미제시 → 파트너 쪽에서 거부되면 그때야 발견). “완료 = 200"이 아니라 gateway access log 경유 증명이 완료 조건이고, REGISTRY_ONLY로 우회 자체를 막아야 한다(passthrough 가이드 §7·§9, T47 실측 — “외부” host가 클러스터 내부 Service로 resolve되는 엣지케이스는 구조적으로 gateway를 못 거친다).
함정 4 — SNI/host 3자 정렬 붕괴. DR-hop1 sni ≠ Gateway hosts[] → gateway filter chain 매칭 실패(연결 리셋). VS의 mesh-측 host ≠ 앱이 부르는 호스트 → 함정 3으로 회귀. 같은 문자열이 여러 리소스에 흩어지는 구조이므로 정렬 지도를 만들어 관리한다(§5 YAML 주석).
함정 5 — secret 위치/키 이름 불일치. secret이 gateway namespace에 없거나 키가 tls.crt/tls.key/ca.crt가 아니면 gateway가 cert를 못 찾아 홉 2 handshake가 실패한다. istioctl proxy-config secret deploy/istio-egressgateway -n istio-system으로 SDS 로드 여부를 먼저 확인한다.
5. 전체 YAML — 최소 구성 한 벌
homelab 컨벤션(gateway Service 443 → targetPort 8443, Gateway CRD는 Envoy가 실제 bind하는 컨테이너 포트 8443 선언 — 신원 통제 가이드 §4의 포트 모델)을 따른다. 공식 task는 번들 gateway 전제로 443을 선언하니, 자기 환경의 Service/targetPort에 맞춰 정렬할 것.
# 0) secret (사전 생성 — §3.1의 kubectl 명령. gateway와 같은 ns 필수)
# 1) ServiceEntry — 외부 host를 레지스트리에 등록 (REGISTRY_ONLY 화이트리스트)
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 } # gateway가 mTLS를 "시작"할 목적지 포트
resolution: DNS
location: MESH_EXTERNAL
---
# 2) Gateway — egress pod에 ISTIO_MUTUAL 종단 서버 (홉 1의 서버 쪽 절반)
apiVersion: networking.istio.io/v1
kind: Gateway
metadata: { name: egress-partner, namespace: istio-system }
spec:
selector: { istio: egressgateway } # gateway pod 라벨과 일치 필수
servers:
- port: { number: 8443, name: https-partner, protocol: HTTPS } # 컨테이너 포트
hosts: [api.partner.example.com] # == DR-hop1 sni == VS hosts (3자 정렬)
tls:
mode: ISTIO_MUTUAL # client cert 강제 + SPIFFE 검증 + 종단
---
# 3) DR-hop1 — sidecar→gateway를 메시 mTLS로 래핑 (호출자 sidecar cluster에 컴파일)
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata: { name: egressgateway-partner, namespace: istio-system }
spec:
host: istio-egressgateway.istio-system.svc.cluster.local # gateway Service FQDN (외부 host 아님!)
subsets:
- name: partner # labels 없는 subset = 채널별 정책·SNI 앵커
trafficPolicy:
portLevelSettings:
- port: { number: 443 } # Service 포트 기준
tls:
mode: ISTIO_MUTUAL # sidecar가 SPIFFE cert 제시 (Gateway와 짝)
sni: api.partner.example.com # gateway filter chain 매칭 키
connectionPool: # 이 홉 전용 — sidecar pod당 상한
tcp:
maxConnections: 1024 # pod당이라 여유 있게. 기본 벽을 명시로 대체
connectTimeout: 3s # in-cluster 홉 — 빠른 실패
tcpKeepalive: { time: 300s, interval: 30s, probes: 3 } # 유령 연결 정리 (Duration — 단위 필수, §검증 기록)
---
# 4) DR-hop2 — gateway→외부 mTLS origination (gateway cluster에 컴파일)
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata: { name: partner-originate-mtls, namespace: istio-system } # gateway ns = secret ns
spec:
host: api.partner.example.com # ServiceEntry hosts와 정확 일치
exportTo: ["."] # gateway ns로 한정 — sidecar 오적용 차단 (위생)
trafficPolicy:
portLevelSettings:
- port: { number: 443 }
tls:
mode: MUTUAL # 사용자 인증서 mTLS
credentialName: partner-client-credential # istio-system의 SDS secret
sni: api.partner.example.com # 외부 vhost/LB 매칭
subjectAltNames: [api.partner.example.com] # 서버 SAN 핀 고정 (오라우팅 잠금)
connectionPool: # 이 홉 전용 — 전사 합류 지점이므로 도출식으로
tcp:
maxConnections: 4096 # peak 동시연결 x 2~3 (측정 기반)
connectTimeout: 3s # 외부 장애 시 빠른 실패 (기본 10s)
idleTimeout: 1800s # 채널 최장 유휴보다 길게 — keepalive로는 못 막음
maxConnectionDuration: 3600s # 수명 상한 = cert 로테이션 반영 + 재분배
tcpKeepalive: { time: 300s, interval: 30s, probes: 3 } # FW idle(1800s)의 1/3 (Duration — 단위 필수)
http:
idleTimeout: 900s # HTTP 풀 유휴 정리 (기본 1h)
maxRequestsPerConnection: 1000 # 커넥션 자연 교체 주기 (1은 keepalive off라 과격)
outlierDetection: # 외부가 다중 IP일 때만! 단일 IP면 ejection=전체 차단
consecutive5xxErrors: 5
interval: 30s
baseEjectionTime: 60s
maxEjectionPercent: 50
---
# 5) VirtualService — 두 홉을 잇는 라우팅 (둘 다 http: gateway가 종단하므로 L7 라우트)
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata: { name: partner-via-egress, namespace: istio-system }
spec:
hosts: [api.partner.example.com]
gateways: [mesh, istio-system/egress-partner]
http:
- match:
- gateways: [mesh] # 홉 1: 모든 sidecar에서
port: 80 # 앱은 평문 http:// 호출
route:
- destination:
host: istio-egressgateway.istio-system.svc.cluster.local
subset: partner # -> DR-hop1 (ISTIO_MUTUAL + sni 적재)
port: { number: 443 } # gateway Service 포트
- match:
- gateways: [istio-system/egress-partner] # 홉 2: gateway에서 종단 후
port: 8443 # Gateway CRD 서버 포트
route:
- destination:
host: api.partner.example.com # -> DR-hop2 (MUTUAL origination)
port: { number: 443 }
weight: 100
정렬 지도 — 한 글자라도 어긋나면 조용히 깨지는 문자열들:
외부 host : SE.hosts == GW.servers.hosts == VS.hosts == DR-hop1 sni
== DR-hop2 host == DR-hop2 sni/subjectAltNames
gw 대상 : DR-hop1 host(egressgateway FQDN) == VS 홉1 destination.host
subset : DR-hop1 subsets[].name == VS 홉1 destination.subset
포트 체인 : Service 443 -> targetPort 8443 == GW.port == VS 홉2 match port
(DR-hop1 portLevelSettings와 VS 홉1 dest는 Service 포트 443 기준)
secret : DR-hop2 credentialName == secret 이름, secret ns == gateway ns
6. 튜닝 표 — long-lived egress mTLS 연결에 실제로 중요한 DR 필드
도출식의 정본은 TCP 처방전·keepalive 필드 노트. 여기선 이중 mTLS 맥락으로 재정렬한다. 입력 3개를 먼저 측정할 것: 채널 peak 동시연결, 신규 conn/s, 경로상 FW/NAT/LB의 idle timeout.
| 필드 | 어느 홉 DR에 | 도출 규칙 | 왜 (메커니즘) |
|---|---|---|---|
tcp.tcpKeepalive.time |
홉 2 (필요시 홉 1도) | FW/NAT idle timeout의 1/3 이하 (예: 1800s → 300) | 중간장비가 유휴 세션을 조용히 버리면 half-open — 다음 요청이 RST. probe(빈 ACK)의 왕복이 세션 타이머를 갱신. 1/3은 probe 유실 1~2회를 견디는 여유 |
tcp.tcpKeepalive.interval/probes |
〃 | 30s × 3 (= 90s 내 사망 판정) | 죽은 상대 감지 전용 다이얼 — 커널 기본(75s×9≈11분)이면 유령 연결이 maxConnections 슬롯을 점유 |
tcp.idleTimeout |
홉 2 | 채널 최장 유휴 간격보다 길게 (기본 1h) | keepalive probe는 데이터가 아니라 이 타이머를 리셋하지 못한다 — “keepalive 넣었는데 정확히 1시간마다 끊겨요"의 정체(T16 실측) |
tcp.connectTimeout |
홉 2: 3s / 홉 1: 3s | 기본 10s는 외부 장애 시 너무 관대 | 파트너 장애 때 gateway 스레드가 연결 수립에 매달리는 시간 = 장애 전파 시간 |
tcp.maxConnections |
홉 2: peak×2~3 / 홉 1: pod당 여유 | 측정 기반. 무한정 금지 | gateway는 전사 합류 지점 — DR 없인 Envoy 기본 1,024 벽(UO flag). 반대로 너무 크면 외부 1곳 지연이 gateway 메모리/FD로 전이(격벽 상실) |
tcp.maxConnectionDuration |
홉 2 | 1h 안팎 (재연결 민감 채널은 제외/연장) | 연결 수명 상한. ① scale-out 후 재분배 유도 ② client cert 로테이션 반영 — TLS 컨텍스트는 신규 handshake에만 적용되므로 상한이 없으면 옛 cert 연결이 무기한 잔존 ③ drain 시 자연 소멸 |
http.maxRequestsPerConnection |
홉 2 | 수백~수천. 1은 keepalive 비활성화라 과격 | 요청 수 기준의 커넥션 자연 교체 — maxConnectionDuration의 보조 다이얼 |
http.idleTimeout |
홉 1·2 | 기본 1h — 풀 회전 주기에 맞게 | 유휴 HTTP 커넥션 정리. 활성 요청 없음 기준 |
outlierDetection |
홉 2 | 다중 IP 목적지일 때만 | 단일 IP 외부에 켜면 ejection = 그 채널 전체 차단(T14 실측) |
값 변경 후엔 반드시 비소급 원칙을 기억할 것 — 소켓 옵션·TLS 컨텍스트는 연결 생성 시 1회 적용이라
기존 연결은 옛 설정으로 산다. maxConnectionDuration이 있으면 자연 교체되고, 없으면 rollout
restart가 필요하다. 검증 절차(설정 저장 → cluster 결부 → stats → 소켓 ss -tno)는
keepalive 노트 §04가 정본이며, 홉 1은 sidecar에서,
홉 2는 gateway pod에서 확인한다.
핵심 정리
- 이중 mTLS = 홉마다 독립된 mTLS 두 겹. 홉 1은 메시 mTLS(
ISTIO_MUTUAL, 호출자 SPIFFE 검증), 홉 2는 gateway가 시작하는 사용자 인증서 mTLS(MUTUAL+credentialName). gateway가 홉 1을 종단하므로 HTTPS over mTLS와 달리 L7이 보인다. - DR은 두 벌, host가 다르다. DR-hop1 host = gateway Service FQDN(+subset·sni), DR-hop2 host = ServiceEntry 외부 host. 같은 host에 DR이 겹치면 조용한 병합(최고참 승)으로 한쪽이 무시된다.
- connectionPool은 “그 홉의 연결을 여는 프록시"에서 발효된다. 홉 1 설정 = 호출자 sidecar의 cluster(pod당 상한), 홉 2 설정 = gateway의 cluster(전사 합류 — 한도·keepalive·수명 상한의 주 전장).
- credentialName은 gateway 전용 + secret은 gateway ns. 키 이름
tls.crt/tls.key/ca.crt고정,ca.crt가 외부 서버 검증 CA. 파일 마운트 방식은 legacy. - portLevelSettings·subset은 통째 교체. 포트 엔트리에
tls만 적으면 그 포트 cluster의 connectionPool이 기본값으로 회귀 — 트래픽이 타는 포트에 전부 재기재. - 200은 완료가 아니다. 홉 1 매칭이 깨지면
ALLOW_ANY에선 sidecar가 조용히 직접 나간다(이중 mTLS 0겹). gateway access log 경유 증명 +REGISTRY_ONLY까지가 완료 조건.
검증 기록 (2026-07-05 · Istio 1.30.0 / k8s 1.30.6)
T90: 전용 egress gateway(주입 템플릿 inject.istio.io/templates: gateway, 테스트 ns) + client cert를 강제하는 실제 파트너(nginx ssl_verify_client on, mesh 밖)로 본문 배선을 E2E 재현. 원자료: manifest.yaml · run.sh · result.txt · verdict.json
| # | 주장 | 실측 결과 | 판정 |
|---|---|---|---|
| G1 | 홉 1 = ISTIO_MUTUAL (DR-hop1 + Gateway server 짝) |
client 사이드카 cluster에 TLS transport socket(sni=외부 host, 메시 SDS default) 컴파일, gateway가 종단 후 L7 라우팅. curl http:// → 200 |
✅ 지지 |
| G2 | 홉 2 = MUTUAL + credentialName (secret은 gateway ns, tls.crt/tls.key/ca.crt) |
secret → SDS 2리소스(kubernetes://<name> client cert, kubernetes://<name>-cacert 검증 CA)로 컴파일. 파트너 응답이 verify=SUCCESS, dn=CN=egress-client 에코 = client cert 제시·검증 증명. subjectAltNames도 exact 매처로 컴파일 |
✅ 지지 (+보강: §3.1-3 — 전용 gateway는 SA에 secrets RBAC 필수) |
| G3 | connectionPool은 “그 홉을 여는 프록시"에 발효 (홉 1 = 호출자 sidecar, 홉 2 = gateway) | 홉 1 값(max 77, 3s, keepalive 300/30/3)은 client 사이드카의 outbound|8443|partner|... cluster에, 홉 2 값(max 83, keepalive)은 gateway의 outbound|443||api.partner.example cluster에 각각 컴파일 (istioctl proxy-config cluster 양쪽 확인) |
✅ 지지 |
| G3-yaml | §5 YAML tcpKeepalive: { time: 300, interval: 30 } |
CRD validation 즉시 거부(must be of type string) — Duration 필드는 300s/30s 문자열 필수 |
🔬 실측 반증 — 본문 교정 (§5 두 곳) |
| G4 | 함정 3: ALLOW_ANY에서 VS mesh 매칭 부재 시 조용한 우회 |
VS mesh 라우트 제거 → gateway access log 0건 증가(미경유), 트래픽은 사이드카에서 직접 외부로(nginx가 본 peer = client pod IP, mTLS 0겹, 평문이 TLS 포트에 도착해 400) | ✅ 지지 (+정밀화: SE가 그 포트를 등록했으면 PassthroughCluster가 아닌 SE cluster로 직행 — 함정 3에 반영) |
검증 환경 특이사항: ① 전용 gateway가 istio-system 밖이라 credentialName SDS에 SA RBAC(Role secrets get/watch/list)이 추가로 필요했다(없으면 503 UF + Secret is not supplied by SDS — §3.1-3으로 본문 보강). ② 이 클러스터는 검증 시점부터 istiod --domain homelab.local이라 gateway Service의 레지스트리 이름이 dmtls-egress.istio-vt-t90.svc.homelab.local이었다 — DR-hop1 host는 반드시 istioctl proxy-config cluster에 보이는 레지스트리 이름과 일치시켜야 하며, 본문 예시의 svc.cluster.local은 표준(istiod 기본 도메인) 환경 기준이다. ③ 음성 대조로 DR-hop2를 SIMPLE(+CA)로 바꾸면 파트너가 정확히 400 No required SSL certificate was sent(nginx error log client sent no required SSL certificate)를 반환해, 200의 전제가 실제 client cert 검증이었음을 교차 증명했다.
참조
아카이브 내부
- HTTPS over mTLS (ISTIO_MUTUAL) 정본 — 앱 TLS를 보존하는 자매 패턴과의 경계
- Egress 신원 기반 통제 — 홉 1 신원 위에 올리는 AuthorizationPolicy·포트 모델의 출처
- DestinationRule 만들기 — host 매칭·병합 규칙·검증 3단의 정본
- Egress TCP 처방전 — P1~P5 문제별 connectionPool 도출식
- tcpKeepalive 필드 노트 — time/interval/probes 커널 매핑·검증 절차
- Egress gateway HTTPS 가이드 — 경유 증명·REGISTRY_ONLY·우회 함정
출처
- ↗ Istio: Egress Gateways with TLS Origination — mTLS origination 공식 task (secret 생성 위치·Gateway/DR/VS 원형·§3·§5의 근거, 2026-07-05 대조)
- ↗ Istio: DestinationRule reference — ClientTLSSettings(credentialName gateway 전용·secret ns 제약·subjectAltNames), portLevelSettings 비상속, connectionPool 기본값(connectTimeout 10s·idleTimeout 1h·maxRequestsPerConnection 0=무제한) (2026-07-05 대조)
- ↗ Istio: Understanding TLS Configuration · Gateway reference — ISTIO_MUTUAL 종단 의미론 (연결 문서의 검증 기록 경유)