--- title: ServiceEntry 리스너 매칭 — 다섯 명제와 재현 시나리오 date: 2026-07-22 type: runbook domain: istio tags: [service-entry, egress, envoy, sni, istioctl, listener] --- > [!note] Outbound 집약 설계서 §4.2의 동반 검증 문서 > "왜 3128 SE는 `protocol: TCP` + `addresses`가 필요한가"를 다섯 명제(P1~P5)로 정리하고, 각 명제를 파일럿 클러스터에서 직접 재현·검증하는 절차(S1~S5)를 제공함. [Outbound 집약 설계서](/docs/istio/egress/squid-consolidation-guide/) 본문이 `se-listener-matching-verification.md`라는 이름으로 참조하는 문서가 이것임. > > **[→ 설계서 포맷 HTML 판 보기 (se-listener-matching-verification.html)](files/se-listener-matching-verification.html)** > > 이 페이지가 본체이며 HTML 판과 내용 동일. - 실행 환경: Istio 설치된 파일럿/테스트 클러스터 (프로덕션 금지) - 필요 도구: kubectl, istioctl, 테스트용 네임스페이스 - 소요: 약 30~40분 ## 1. 검증된 정리 (5개 명제) 1. **[P1] 443 (https 직결)**: SE를 `protocol: TLS`로 선언하면 리스너에 tls_inspector가 붙고 SE 호스트별 `filterChainMatch.serverNames`(SNI) 체인이 생성됨. 연결 **첫 바이트** (ClientHello)에 이름이 평문으로 존재하므로 종단 없이 이름 판별 가능 → `addresses` 없이 `0.0.0.0_443`이어도 다중 도메인 안전 공존. 2. **[P2] 3128을 TLS로 선언하면 안 되는 이유**: 이 연결의 첫 바이트는 평문 HTTP (`CONNECT …` = 0x43…)라 매칭 시점에 읽을 TLS가 없음 (TLS는 CONNECT/200 이후 = 너무 늦음). 늦게 오는 SNI마저 origin의 이름이라 "Squid행인가?"의 답이 아님. 증상: 어느 serverNames 체인에도 매치 실패 → **연결 리셋**. 3. **[P3] TCP 선언 + addresses의 역할**: HTTP로 선언하면 Envoy가 프록시 대화의 해석 주체가 되므로(B안 전환) TCP로 선언함. TCP는 이름을 읽지 않아 판별자는 목적지 IP뿐 — `addresses` 있으면 IP별 정밀 리스너(`_3128`), **없으면 0.0.0.0_3128 와일드카드**. (TCP 자체가 아니라 "TCP + addresses 부재" 조합이 와일드카드를 만듦) 4. **[P4] 와일드카드의 문제 2단계**: - 1단계 (addresses 없는 3128 SE **1개**): 무관한 모든 3128행 트래픽을 흡입해 이 SE의 경로로 회송 — 제3자 하이재킹. - 2단계 (**2개 이상**): 같은 키 `0.0.0.0_3128`을 복수 SE가 주장하는 정의 충돌 → istiod가 하나만 반영(비결정적) → 다른 SE의 트래픽이 앱이 걸지 않은 목적지(다른 gateway/프록시)로 물리적 오배송. 5. **[P5] 결론**: 평문 TCP 포트의 다중 SE 안전 공존 조건 = SE마다 고유한 `addresses`. 그 값은 "앱이 dial할 IP의 사본"이어야 함 (임의값 불가 — 키 ≠ dial 이면 리스너 미스). 대안: Istio DNS proxying으로 앱의 dial 값 자체를 시스템이 통제. ## 2. 사전 준비 ```bash NS=se-verify kubectl create ns $NS kubectl label ns $NS istio-injection=enabled # 테스트 클라이언트 (sleep 파드) kubectl -n $NS apply -f https://raw.githubusercontent.com/istio/istio/master/samples/sleep/sleep.yaml # (폐쇄망이면 curl 가능한 아무 이미지로 대체) kubectl -n $NS wait --for=condition=ready pod -l app=sleep --timeout=120s POD=$(kubectl -n $NS get pod -l app=sleep -o jsonpath='{.items[0].metadata.name}') ``` - 클러스터의 `outboundTrafficPolicy` 확인 요망 — 미스(리스너에 안 걸린 트래픽)의 결말이 달라짐: - `ALLOW_ANY`(기본): 미스 → PassthroughCluster로 직행 시도 - `REGISTRY_ONLY`: 미스 → 차단 (BlackHoleCluster) ```bash kubectl -n istio-system get configmap istio -o yaml | grep -A2 outboundTrafficPolicy ``` - 아래 IP·도메인은 예시 — 실환경 값으로 치환: - Squid: `squid.gw-zone.example` (GSLB VIP 예: 198.51.100.10, 198.51.100.11) - 무관 IP: `192.0.2.55` (TEST-NET — 어디에도 라우팅되지 않는 안전한 더미) ## 3. 재현 시나리오 ### S1 — [P1] TLS 프로토콜의 SNI 체인 확인 명제: TLS 선언 SE는 addresses 없이도 호스트별 serverNames 체인으로 갈라진다. ```bash kubectl -n $NS apply -f - <<'EOF' apiVersion: networking.istio.io/v1 kind: ServiceEntry metadata: {name: tls-a} spec: hosts: ["one.example.com"] ports: [{number: 443, name: tls, protocol: TLS}] location: MESH_EXTERNAL resolution: DNS --- apiVersion: networking.istio.io/v1 kind: ServiceEntry metadata: {name: tls-b} spec: hosts: ["two.example.com"] ports: [{number: 443, name: tls, protocol: TLS}] location: MESH_EXTERNAL resolution: DNS EOF istioctl proxy-config listeners $POD -n $NS --port 443 -o json \ | jq '[.[] | {name, lf: [.listenerFilters[]?.name], chains: [.filterChains[]? | .filterChainMatch.serverNames]}]' ``` 기대 결과 및 판정: - `name: "0.0.0.0_443"` — 와일드카드 **맞음** (addresses 없으므로) - `lf`에 `envoy.filters.listener.tls_inspector` 존재 - `chains`에 `["one.example.com"]`, `["two.example.com"]` 각각 존재 - **판정**: 와일드카드여도 체인 계층(SNI)이 판별 → 두 도메인 공존 안전 확인 - 참고: 폐쇄망에서는 실제 443 직행이 방화벽에 막히므로 **설정 검증만으로 충분** (트래픽 불요) 정리: `kubectl -n $NS delete se tls-a tls-b` ### S2 — [P2] 3128을 TLS로 선언 → 리셋 재현 명제: 프록시행 트래픽의 첫 바이트는 평문이라 TLS 선언 시 매치 실패로 리셋된다. ```bash kubectl -n $NS apply -f - <<'EOF' apiVersion: networking.istio.io/v1 kind: ServiceEntry metadata: {name: squid-wrong-tls} spec: hosts: ["squid.gw-zone.example"] ports: [{number: 3128, name: tls, protocol: TLS}] # 일부러 잘못 선언 location: MESH_EXTERNAL resolution: DNS EOF # 리스너 확인 — tls_inspector + serverNames 체인이 생겼는지 istioctl proxy-config listeners $POD -n $NS --port 3128 -o json \ | jq '[.[] | {name, lf: [.listenerFilters[]?.name], chains: [.filterChains[]? | .filterChainMatch.serverNames]}]' # 평문 CONNECT 시도 kubectl -n $NS exec $POD -c sleep -- \ curl -m 5 -sv -x http://squid.gw-zone.example:3128 https://api.example.com/ -o /dev/null ``` 기대 결과 및 판정: - 리스너에 tls_inspector + `["squid.gw-zone.example"]` 체인 존재 - curl: `Recv failure: Connection reset by peer` 류 — 첫 바이트 `0x43`("C")이 ClientHello(`0x16`)가 아니라 어느 체인에도 미매치 → 리셋 - **판정**: "TLS 선언은 프록시 트래픽과 양립 불가"의 행동 증거 정리: `kubectl -n $NS delete se squid-wrong-tls` ### S3 — [P3][P4-1단계] TCP + addresses 부재 → 0.0.0.0 + 하이재킹 명제: addresses 없는 TCP SE는 0.0.0.0_3128이 되고 무관한 3128 트래픽을 흡입한다. ```bash kubectl -n $NS apply -f - <<'EOF' apiVersion: networking.istio.io/v1 kind: ServiceEntry metadata: {name: squid-no-addr} spec: hosts: ["squid.gw-zone.example"] ports: [{number: 3128, name: tcp, protocol: TCP}] # addresses 의도적 생략 location: MESH_EXTERNAL resolution: DNS EOF # (1) 와일드카드 확인 istioctl proxy-config listeners $POD -n $NS --port 3128 # 기대: ADDRESSES=0.0.0.0 PORT=3128 DESTINATION=Cluster: outbound|3128||squid.gw-zone.example # (2) 발신측에는 DNS 결과가 있음을 대조 (P3의 "두 테이블" 검증) istioctl proxy-config endpoints $POD -n $NS --cluster "outbound|3128||squid.gw-zone.example" # 기대: 실 VIP가 보임. 반복 실행 시 GSLB 로테이션 따라 바뀜 — 그래도 (1)은 0.0.0.0 그대로 # (3) 하이재킹: SE와 무관한 IP:3128 kubectl -n $NS exec $POD -c istio-proxy -- pilot-agent request GET stats \ | grep 'outbound|3128||squid' | grep upstream_cx_total # 사전값 기록 kubectl -n $NS exec $POD -c sleep -- sh -c 'curl -m 4 -s telnet://192.0.2.55:3128; echo rc=$?' kubectl -n $NS exec $POD -c istio-proxy -- pilot-agent request GET stats \ | grep 'outbound|3128||squid' | grep upstream_cx_total # 사후값 비교 ``` 기대 결과 및 판정: - 라우팅 불가 주소(192.0.2.55)인데 연결이 **성립**하거나, squid 클러스터의 `upstream_cx_total`이 **증가** — 트래픽이 squid 클러스터로 회송된 증거 - **판정**: 와일드카드 = "3128 전부 흡입" 의미론 확인. (앱이 받는 최종 응답은 Squid의 파싱 결과에 따라 400/유사 — 원인 로그가 어디에도 안 남는 침묵 오배송) ### S4 — [P4-2단계] 와일드카드 2개 → 정의 충돌·오배송 명제: addresses 없는 3128 SE가 둘이면 하나만 반영되고, 진 쪽 트래픽은 오배송된다. ```bash # S3의 squid-no-addr 유지한 채 두 번째 추가 kubectl -n $NS apply -f - <<'EOF' apiVersion: networking.istio.io/v1 kind: ServiceEntry metadata: {name: other-proxy-no-addr} spec: hosts: ["other-proxy.partner.example"] ports: [{number: 3128, name: tcp, protocol: TCP}] location: MESH_EXTERNAL resolution: DNS EOF # (1) 리스너는 여전히 1개 — 어느 클러스터를 가리키는지 확인 istioctl proxy-config listeners $POD -n $NS --port 3128 # 기대: 0.0.0.0_3128 한 줄뿐, DESTINATION은 둘 중 하나만 # (2) 충돌 검출 istioctl analyze -n $NS kubectl -n istio-system logs deploy/istiod --since=5m | grep -i conflict | head ``` 기대 결과 및 판정: - 리스너 1개, destination은 둘 중 하나 (버전에 따라 승자 상이 — 비결정성 자체가 논점) - analyze 또는 istiod 로그에 충돌 경고 (메시지 형식은 버전 의존) - **판정**: "둘 다 등록됨"이 아니라 "하나가 다른 하나를 침묵 속에 삼킴"을 확인. 진 쪽 host로 dial한 트래픽이 이긴 쪽 destination으로 가는 오배송 성립 정리: `kubectl -n $NS delete se other-proxy-no-addr` ### S5 — [P5] addresses 기입 → 정밀 리스너·하이재킹 소멸 ```bash kubectl -n $NS apply -f - <<'EOF' apiVersion: networking.istio.io/v1 kind: ServiceEntry metadata: {name: squid-no-addr} spec: hosts: ["squid.gw-zone.example"] addresses: ["198.51.100.10/32", "198.51.100.11/32"] # GSLB VIP 집합 (실환경 값) ports: [{number: 3128, name: tcp, protocol: TCP}] location: MESH_EXTERNAL resolution: DNS EOF istioctl proxy-config listeners $POD -n $NS --port 3128 # 기대: 198.51.100.10:3128, 198.51.100.11:3128 — VIP별 리스너, 0.0.0.0 소멸 kubectl -n $NS exec $POD -c sleep -- sh -c 'curl -m 4 -s telnet://192.0.2.55:3128; echo rc=$?' # 기대: 이제 미스 → ALLOW_ANY면 직행 시도 후 timeout, REGISTRY_ONLY면 즉시 차단 # (S3과 달리 squid 클러스터 카운터가 증가하지 않음) ``` **판정**: addresses = "잡는 범위를 의도한 좌표로 못 박는 격리"의 완성 확인. ### 정리 ```bash kubectl delete ns $NS ``` ## 4. 결과 기록표 | 시나리오 | 명제 | 확인 항목 | 결과 (기록) | |---|---|---|---| | S1 | P1 | 0.0.0.0_443 + tls_inspector + serverNames 체인 2개 | | | S2 | P2 | tls_inspector 존재 · 평문 CONNECT 리셋 | | | S3 | P3, P4-1 | 0.0.0.0_3128 · endpoints엔 실 VIP · 무관 IP 흡입 | | | S4 | P4-2 | 리스너 1개 · destination 편중 · 충돌 경고 | | | S5 | P5 | VIP별 리스너 · 무관 IP 미스 전환 | | 주의사항: - 반드시 파일럿/테스트 클러스터에서 수행 (S3~S4는 해당 네임스페이스의 3128 트래픽에 실영향) - istioctl 출력 형식·analyze 메시지는 Istio 버전에 따라 다를 수 있음 — 구조(키·체인·destination)로 판정할 것 - S3의 (2)는 "DNS 결과는 클러스터에만 유입되고 리스너 키에는 끝내 반영되지 않는다"는 두-테이블 명제의 직접 증거이므로 반복 실행으로 IP 변화까지 관찰 권장 ## 관련 문서 - [Outbound 집약 설계서 — Squid 경로 일원화](/docs/istio/egress/squid-consolidation-guide/) ([원문 HTML](/docs/istio/egress/squid-consolidation-guide/files/egress-gateway-squid-consolidation-guide.html)) — §4.2가 이 문서의 다섯 명제를 본문 논거로 사용. 이 문서는 그 명제들의 재현 절차 - [DNS/GSLB resolution 재현 랩](/docs/istio/egress/dns-gslb-repro-lab/) — 두-테이블 중 발신(클러스터, `resolution: DNS`) 쪽의 실측 - [Egress 4-CRD 멘탈모델 — curl 한 번 = 두 hop](/docs/istio/egress/crd-mental-model/) — SE·DR·Gateway·VS 배선의 원리 - [Istio ServiceEntry와 Envoy 설정 반영 범위 — sidecar vs gateway, 그리고 DestinationRule](/docs/istio/egress/se-envoy-config-scope/) — 이 명제들의 상위 맥락: SE 필드별 xDS 반영 위치와 sidecar/gateway 스코핑 차이