homelab89 Docs Logs Legacy Files ☰ TOC 🌓
guideistio 2026-07-05egressmtlsdestinationruleconnection-pool

이중 mTLS Egress — 홉마다 DR 한 벌, 커넥션 설정은 그 홉을 여는 프록시에 박힌다

ABSTRACT

이중 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. 홉 1 (sidecar → egressgateway): sidecar가 요청을 메시 mTLS(ISTIO_MUTUAL)로 래핑. gateway는 이걸 종단하며 호출 워크로드의 SPIFFE 신원을 검증한다 — “누가 나가는가"의 확정 지점.
  2. 홉 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 함정 표).

snisubjectAltNames의 역할:

  • tls.sni — sidecar가 만드는 outer ClientHello의 SNI에 원본 목적지 호스트를 적재한다. gateway listener가 filter chain(어느 server/채널인가)을 고르는 매칭 키이므로, DR sni == Gateway hosts[] == 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.tcpKeepalivesidecar→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):

  1. gateway 워크로드 전용이다 — sidecar는 이 필드를 쓸 수 없다(파일 경로 방식만 가능). “외부 mTLS의 client cert는 gateway에서 중앙 관리"라는 이 패턴의 구도가 여기서 강제된다.
  2. secret은 gateway가 있는 namespace에 있어야 SDS가 찾는다. egressgateway가 istio-system이면 secret도 istio-system.
  3. gateway pod의 ServiceAccount에 secret 읽기 RBAC이 있어야 istiod가 credentialName을 서빙한다 — istiod는 SDS 요청을 SubjectAccessReview로 인가하므로, gateway SA에 해당 ns의 secrets get/watch/list Role이 없으면 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.yamldmtls-egress-sds Role 참고).
# 파트너가 발급/승인한 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 } }
WARNING

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 flag UO, 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 clusterdr: 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 실측)
TIP

값 변경 후엔 반드시 비소급 원칙을 기억할 것 — 소켓 옵션·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)은 gatewayoutbound|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 검증이었음을 교차 증명했다.


참조

아카이브 내부

출처

Files