ServiceEntry 리스너 매칭 — 다섯 명제와 재현 시나리오
“왜 3128 SE는 protocol: TCP + addresses가 필요한가"를 다섯 명제(P1P5)로 정리하고, 각 명제를 파일럿 클러스터에서 직접 재현·검증하는 절차(S1S5)를 제공함. Outbound 집약 설계서 본문이 se-listener-matching-verification.md라는 이름으로 참조하는 문서가 이것임.
→ 설계서 포맷 HTML 판 보기 (se-listener-matching-verification.html)
이 페이지가 본체이며 HTML 판과 내용 동일.
- 실행 환경: Istio 설치된 파일럿/테스트 클러스터 (프로덕션 금지)
- 필요 도구: kubectl, istioctl, 테스트용 네임스페이스
- 소요: 약 30~40분
1. 검증된 정리 (5개 명제)
-
[P1] 443 (https 직결): SE를
protocol: TLS로 선언하면 리스너에 tls_inspector가 붙고 SE 호스트별filterChainMatch.serverNames(SNI) 체인이 생성됨. 연결 첫 바이트 (ClientHello)에 이름이 평문으로 존재하므로 종단 없이 이름 판별 가능 →addresses없이0.0.0.0_443이어도 다중 도메인 안전 공존. -
[P2] 3128을 TLS로 선언하면 안 되는 이유: 이 연결의 첫 바이트는 평문 HTTP (
CONNECT …= 0x43…)라 매칭 시점에 읽을 TLS가 없음 (TLS는 CONNECT/200 이후 = 너무 늦음). 늦게 오는 SNI마저 origin의 이름이라 “Squid행인가?“의 답이 아님. 증상: 어느 serverNames 체인에도 매치 실패 → 연결 리셋. -
[P3] TCP 선언 + addresses의 역할: HTTP로 선언하면 Envoy가 프록시 대화의 해석 주체가 되므로(B안 전환) TCP로 선언함. TCP는 이름을 읽지 않아 판별자는 목적지 IP뿐 —
addresses있으면 IP별 정밀 리스너(<IP>_3128), 없으면 0.0.0.0_3128 와일드카드. (TCP 자체가 아니라 “TCP + addresses 부재” 조합이 와일드카드를 만듦) -
[P4] 와일드카드의 문제 2단계:
- 1단계 (addresses 없는 3128 SE 1개): 무관한 모든 3128행 트래픽을 흡입해 이 SE의 경로로 회송 — 제3자 하이재킹.
- 2단계 (2개 이상): 같은 키
0.0.0.0_3128을 복수 SE가 주장하는 정의 충돌 → istiod가 하나만 반영(비결정적) → 다른 SE의 트래픽이 앱이 걸지 않은 목적지(다른 gateway/프록시)로 물리적 오배송.
-
[P5] 결론: 평문 TCP 포트의 다중 SE 안전 공존 조건 = SE마다 고유한
addresses. 그 값은 “앱이 dial할 IP의 사본"이어야 함 (임의값 불가 — 키 ≠ dial 이면 리스너 미스). 대안: Istio DNS proxying으로 앱의 dial 값 자체를 시스템이 통제.
2. 사전 준비
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)
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 — 어디에도 라우팅되지 않는 안전한 더미)
- Squid:
3. 재현 시나리오
S1 — [P1] TLS 프로토콜의 SNI 체인 확인
명제: TLS 선언 SE는 addresses 없이도 호스트별 serverNames 체인으로 갈라진다.
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 선언 시 매치 실패로 리셋된다.
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 트래픽을 흡입한다.
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가 둘이면 하나만 반영되고, 진 쪽 트래픽은 오배송된다.
# 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 기입 → 정밀 리스너·하이재킹 소멸
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 = “잡는 범위를 의도한 좌표로 못 박는 격리"의 완성 확인.
정리
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 경로 일원화 (원문 HTML) — §4.2가 이 문서의 다섯 명제를 본문 논거로 사용. 이 문서는 그 명제들의 재현 절차
- DNS/GSLB resolution 재현 랩 — 두-테이블 중 발신(클러스터,
resolution: DNS) 쪽의 실측 - Egress 4-CRD 멘탈모델 — curl 한 번 = 두 hop — SE·DR·Gateway·VS 배선의 원리
- Istio ServiceEntry와 Envoy 설정 반영 범위 — sidecar vs gateway, 그리고 DestinationRule — 이 명제들의 상위 맥락: SE 필드별 xDS 반영 위치와 sidecar/gateway 스코핑 차이