--- title: Egress 외부 endpoint 프로토콜별 Istio 설정 — HTTP vs HTTPS date: 2026-06-07 type: guide domain: istio tags: [egress, protocol-selection, tls-origination, service-entry, envoy] --- > [!abstract] > 외부로 나가는 트래픽의 **endpoint가 평문 HTTP일 때**와 **HTTPS일 때** ServiceEntry / Gateway / VirtualService / DestinationRule을 어떻게 설정하고, **왜 한쪽 설정을 다른 쪽에 그대로 쓰면 깨지는지**를 Envoy 필터 체인 수준에서 정리한다. 결론은 한 문장: 외부 endpoint가 **첫 바이트에 실제로 말하는 프로토콜**과 ServiceEntry port `protocol`이 일치해야 하고, 평문에 TLS 설정을 쓰면 **istiod가 그 listener에 거는 필터 체인 자체가 달라져** connection reset·핸드셰이크 실패로 깨진다. (검증 기준 Istio 1.30 / `networking.istio.io/v1`, sidecar 모드) --- ## 1. 배경 — 왜 "외부가 무엇을 받느냐"가 모든 설정을 가른다 egress 설정을 처음 만지면 "TLS 옵션 몇 개를 켜고 끄는 문제"처럼 보인다. 그래서 HTTPS 예제에서 동작하던 매니페스트를 평문 endpoint에 복붙하고, `protocol: TLS`/`sniHosts`만 남겨둔 채 connection reset에 빠진다. 이 문서가 풀려는 혼란이 바로 이것이다. 핵심은 egress에서 **두 개의 독립된 사실**이 자꾸 한 단어("https")로 뭉뚱그려진다는 점이다. - **외부 endpoint가 받는 것** — 파트너 서버가 80에서 평문을 받나, 443에서 TLS를 받나. 이건 **상대가 정한 고정값**이다. - **앱이 보내는 것** — 우리 앱 코드가 `http://`를 부르나 `https://`를 부르나. 이건 **우리가 바꿀 수 있는 값**이다. 이 둘이 같을 필요는 없다. 앱이 `http://`를 보내도 Istio가 중간에서 외부로 새 TLS를 맺어줄 수 있다(origination). 그래서 "https로 나가야 하나요?"라는 질문은 사실 **"외부 endpoint가 TLS를 받아주는가?"** 라는, 우리가 못 바꾸는 쪽 사실로 환원된다. 받아주면 TLS를 쓸 수 있고(권장), 안 받아주면 평문 외엔 선택지가 없다. > [!note] > 선행 개념: ServiceEntry(외부 서비스를 mesh 레지스트리에 등록), TLS origination(앱은 평문, Istio가 외부로 TLS를 새로 맺음), passthrough(앱의 TLS를 Istio가 안 건드리고 SNI로만 라우팅). 이 셋의 CRD 골격은 [Egress gateway 매뉴얼](/docs/istio/egress/gateway/)에 정본이 있고, 여기서는 **HTTP vs HTTPS 경계**만 깊게 판다. ### 네 가지 경로로 좌표를 잡기 "외부 endpoint가 무엇을 받느냐 × 앱이 무엇을 보내느냐"의 조합이 실무 패턴 네 가지를 만든다. ①②③은 모두 **외부가 HTTPS**라 "TLS를 누가·어디서 종단하느냐"의 차이일 뿐이고, ④만 **외부가 평문 HTTP**라 TLS가 아예 없다 — 이 문서가 추가로 채우는 칸이 ④다. | # | 외부 endpoint | 앱 호출 | 패턴 | 외부 구간 암호화 | GW L7 가시성 | 앱 변경 | |---|---|---|---|---|---|---| | ① | HTTPS | `https://` | **passthrough** | end-to-end 유지 | ❌ (SNI/L4만) | 없음 | | ② | HTTPS | `http://` | **mTLS origination** (MUTUAL) | GW부터 새 TLS | ✅ 전부 | 필요 | | ③ | HTTPS | `http://` | **TLS origination** (SIMPLE) | GW부터 새 TLS | ✅ 전부 | 필요 | | ④ | **HTTP (평문)** | `http://` | **plain HTTP** | ❌ 없음(평문 노출) | ✅ 전부 | 없음 | ②③이 앱을 `http://`로 바꾼다고 ④와 같아지지 않는다 — ②③의 외부 구간은 여전히 TLS다. 이 착각이 평문 노출 사고의 단골 원인이다(→ §6 What you might be missing). --- ## 2. 핵심 — `protocol` 선언이 Envoy 필터 체인을 고른다 **멘탈 모델 앵커 한 문장:** ServiceEntry/Gateway 포트의 `protocol` 값은 옵션이 아니라 **스위치**다 — istiod가 그 listener(LDS)에 **어떤 필터 체인을 박을지**를 고르고, 필터 체인은 **입력 첫 바이트의 형태**를 전제한다. 그래서 선언한 protocol과 실제로 들어오는 첫 바이트가 어긋나면, 옵션이 안 맞는 게 아니라 **체인 자체가 못 맞물려** 깨진다. [CR→xDS 멘탈 모델](/docs/istio/xds-envoy/cr-xds-model/)의 원리 그대로다: **CR은 입력, 진실은 Envoy config.** `protocol`을 바꾸면 같은 포트라도 전혀 다른 listener가 생성된다. | `protocol` | 입력 첫 바이트 | 핵심 필터 체인 | 라우팅 키 | 보이는 메트릭 | |---|---|---|---|---| | **HTTP** | `GET / HTTP/1.1...` (평문) | HTTP Connection Manager | Host 헤더 (RDS) | `istio_requests_total` (method/path/status) | | **TLS** | TLS ClientHello | `tls_inspector` → `tcp_proxy` | SNI | `istio_tcp_*` 만 (복호화 안 함) | | **HTTPS** | 수신(inbound): TLS ClientHello / 발신(origination): 평문 | 수신: TLS 종단(복호화) → HCM. **발신 leg은 반대** — HCM이 파싱한 평문을 CDS 클러스터가 새 TLS로 origin(암호화) | 복호 후 Host 헤더 | `istio_requests_total` (origination 자체가 아니라 그 앞단 HCM 파싱 지점에서 잡힘) | > [!bug] 실측·문헌 정정 — "TLS 종단(복호화) → HCM"은 방향에 따라 반대로 읽힌다 > 원래 표는 이 필터 체인 설명을 "주로 origination 외부 leg"(게이트웨이/사이드카 → 외부 서버)에도 그대로 적용했는데, 이는 두 방향을 혼동한 것이다. Envoy 아키텍처상 **TLS termination(복호화)은 listener(downstream, 서버 역할)**, **TLS origination(암호화)은 cluster(upstream, 클라이언트 역할)**에서 일어나는 별개의 방향이다([Envoy TLS 아키텍처](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/security/ssl)). "TLS 종단(복호화)"은 게이트웨이가 내부 mTLS(사이드카→게이트웨이) 연결을 받는 **인바운드**에만 맞는 말이다. origination 외부 leg에서는 정반대로 동작한다 — 이미 들어온 TLS를 복호화하는 게 아니라, DestinationRule의 `tls.mode: SIMPLE/MUTUAL`을 만난 CDS 클러스터가 클라이언트 역할로 새 TLS 핸드셰이크를 originate(암호화)한다. 즉 origination 외부 leg는 listener의 TLS termination이 아니라 cluster의 UpstreamTlsContext에 의한 TLS **발신**이며, "복호화"가 아니라 "암호화"다. `istio_requests_total`도 이 leg 자체가 아니라 그 앞단(사이드카/게이트웨이가 평문 HTTP 요청을 HCM으로 파싱하는 지점)에서 잡힌다. 이 표가 왜 호환 불가를 만드는지는 **첫 바이트**를 보면 즉시 드러난다. 평문 HTTP가 와이어에 처음 흘리는 것은: ``` GET /v1/foo HTTP/1.1\r\nHost: api.partner.example.com\r\n... ``` 이건 **TLS ClientHello가 아니다.** 그런데 이 endpoint를 `protocol: TLS`로 등록하면 Envoy는 그 포트에 `tls_inspector`를 걸고 첫 바이트에서 SNI를 뽑으려 한다 → `GET ...`에는 ClientHello 구조가 없으니 **SNI 추출 실패 → 매칭되는 filter chain 없음 → connection reset**(또는 SNI 없는 filter chain으로 떨어져 미스매치). `protocol: HTTPS`로 쓰면 Envoy가 TLS 핸드셰이크를 기대하다 평문을 받아 **핸드셰이크 자체가 깨진다.** 반대도 대칭이다 — 외부가 HTTPS인데 `protocol: HTTP`로 쓰면 Envoy가 암호문을 HTTP로 파싱하려다 깨진다. 정확한 response flag 해석(`NR`/downstream reset 등은 컨텍스트에 따라 달라짐)은 [Envoy response flags](/docs/istio/xds-envoy/envoy-response-flags/)에 위임한다. > [!bug] 실측 정정 (T17, 2026-07-05 · homelab Istio 1.30.0) — `protocol: TLS` 오등록이 항상 connection reset은 아니다 > 위 문단의 "SNI 추출 실패 → 매칭되는 filter chain 없음 → connection reset" 서술은 **`ALLOW_ANY`(기본 outboundTrafficPolicy)에서는 재현되지 않는다.** 평문 80 포트를 `protocol: TLS`로 오등록한 뒤 실제 외부 호스트(httpbin.org)로 호출하며 `0.0.0.0_80` 리스너를 까보면, TLS ServiceEntry가 추가한 `serverNames` 기반 SNI 전용 filterChain **옆에** `ALLOW_ANY`가 모든 outbound 리스너에 항상 깔아두는 `raw_buffer` + `http/1.1`/`h2c` catch-all filterChain이 그대로 남아 있다. SNI가 없는 평문 요청은 SNI 전용 체인에 애초에 매치되지 않고 기존 catch-all 체인으로 흘러 **익명 HTTP 패스스루로 그냥 성공한다**(실측: `misdeclared_tls=200`, `exit=0`). 즉 `protocol: TLS` 오등록은 기존 평문 경로를 막는 게 아니라 SNI 경로 하나를 **병행 추가**할 뿐이다 — connection reset은 이 catch-all 체인 자체가 없는 환경(예: `REGISTRY_ONLY`이거나 포트가 애초에 HTTP로 감지되지 않는 경우)에서만 기대할 수 있다. ```mermaid flowchart TD A["plain HTTP bytes
GET / HTTP/1.1"] --> B{"port protocol?"} B -->|HTTP| OK["HTTP Conn Manager
parse + route by Host
L7 visible"] B -->|TLS| C["tls_inspector
expects ClientHello"] B -->|HTTPS| D["TLS terminate
expects handshake"] C --> E["not a ClientHello
no SNI"] E --> F["ALLOW_ANY: falls through to
existing catch-all chain (succeeds)
REGISTRY_ONLY/no catch-all: reset"] D --> G["handshake fails on
plaintext input"] ``` > [!warning] > **`protocol`은 "외부 endpoint가 첫 바이트에 실제로 무엇을 말하는가"와 반드시 일치해야 한다.** Istio에 protocol sniffing(자동 감지)이 있긴 하지만, ServiceEntry에 `protocol`을 **명시하면 그 명시값이 우선**하므로 불일치는 그대로 장애가 된다. 이게 "옵션 켜고 끄기"가 아닌 이유다. ### 객체별로 무엇이 어떻게 갈리나 (척추 표) 위 스위치가 4개 CRD로 어떻게 번지는지의 지도다. ④ 평문 열만 보면 "TLS 흔적이 전부 빠진" 모습이 한눈에 들어온다. | 객체 | ① passthrough | ②③ origination | ④ plain HTTP | |---|---|---|---| | **ServiceEntry** `ports.protocol` | `TLS` | `HTTP`(앱 leg) + `HTTPS`(외부 leg) | **`HTTP`** | | **VirtualService** 라우팅 | `tls:` + `sniHosts` | `http:` | **`http:`** | | **Gateway** server (GW 경유 시) | `TLS` / `tls.mode: PASSTHROUGH` | `HTTPS` / `ISTIO_MUTUAL` | **`HTTP`** (또는 내부 leg만 mTLS) | | **DestinationRule** 외부 leg `tls.mode` | 없음 | `MUTUAL`(②) / `SIMPLE`(③) | **없음** (평문 그대로) | | client 인증서(SDS) | 앱(필요 시) | 게이트웨이 | 불필요 | --- ## 3. 외부가 HTTPS인 경우 — 세 갈래의 요약 (정본은 별도) 외부가 TLS를 받는 경우는 "TLS를 누가·어디서 종단하느냐"로 ①/③/②로 갈린다. 전체 매니페스트·TLS 모드 정밀 비교는 [Egress gateway 매뉴얼](/docs/istio/egress/gateway/)이 정본이므로(passthrough §04, mTLS origination §06, TLS 모드 비교 §07) 여기서는 **④ 평문과 대비되는 차이점만** 한 줄씩 남긴다. - **① passthrough** — 앱 `https://` 직접, ServiceEntry `protocol: TLS`. end-to-end 암호화를 유지하되 GW가 암호문만 보므로 **L7 가시성 0**(SNI/L4만). (정본 §04) - **② MUTUAL origination** — ③ + 게이트웨이가 client cert를 제시(`credentialName`). 파트너가 mTLS를 요구할 때. (정본 §06) - **③ SIMPLE origination** — 앱 `http://`, Istio가 외부로 새 TLS를 originate(외부 server cert만 검증). **④와의 결정적 차이: SIMPLE은 외부 구간 평문 노출을 없앤다** — 외부가 HTTPS를 받아주는 한, ④ plain HTTP보다 **항상 권장**. (정본 §07) 핵심만 추리면: ③ SIMPLE은 "앱은 평문, 외부 구간은 TLS"다. ④는 "앱도 평문, 외부 구간도 평문"이다. 둘을 가르는 건 오직 **외부 endpoint가 TLS를 받아주느냐** 하나뿐이다. --- ## 4. 외부가 HTTP(평문)인 경우 — 설정 방법 (④) 외부 서버가 80 포트에서 평문 HTTP를 받는 경우. **TLS가 아예 없으므로** §3의 어떤 TLS 설정도 쓰면 안 된다. 역설적으로 평문이라 사이드카/게이트웨이가 **L7(method/path/status)을 그냥 다 본다** — passthrough를 괴롭히던 "암호문이라 L4만" 제약이 여기엔 없다. plain HTTP의 거의 유일한 보상이 이 공짜 L7 가시성이다. ### 4-1. 사이드카 직접 egress (게이트웨이 없음, 기본형) ServiceEntry 하나면 끝이다. ```yaml apiVersion: networking.istio.io/v1 kind: ServiceEntry metadata: name: partner-http namespace: istio-system # 전역 가시성. 앱 ns에 둬도 됨 spec: hosts: [api.partner.example.com] ports: - number: 80 name: http # name 접두사도 프로토콜 힌트(http-) protocol: HTTP # ← 핵심. TLS/HTTPS 아님 resolution: DNS location: MESH_EXTERNAL ``` 앱이 `http://api.partner.example.com`을 부르면 사이드카가 §2 표의 HTTP 행대로 **HTTP cluster로 인식해** 내보낸다. 줄별 "왜": - `protocol: HTTP` — istiod가 listener에 HTTP Connection Manager를 박게 하는 단 한 줄. 이게 TLS/HTTPS면 위에서 본 대로 깨진다. - `resolution: DNS` + `location: MESH_EXTERNAL` — Envoy가 `hosts`를 DNS로 풀어 mesh 밖 endpoint로 취급. - 두 환경 차이가 중요하다: - `REGISTRY_ONLY` 환경이면 이 ServiceEntry가 **반드시** 있어야 BlackHole로 안 떨어진다. - `ALLOW_ANY`면 ServiceEntry 없이도 나가지만 **PassthroughCluster**로 빠진다 — 단, 그 포트가 HTTP로 감지(protocol sniffing)되면 `istio_requests_total` 자체는 **여전히 증가한다**(`destination_service`엔 실제 호스트명이 찍히지만, `destination_service_name`은 뭉뚱그려진 `PassthroughCluster`로 남는다 — 실측: T17). "L7 메트릭이 안 잡힌다"는 표현은 과장이고, ServiceEntry를 두는 진짜 실익은 L7 메트릭의 유무가 아니라 **목적지(host) 단위 가시성 + 라우팅/정책 대상화**다 — L7이 완전히 사라지는 건 트래픽이 애초에 HTTP로 감지되지 않아 순수 TCP로 프록시될 때뿐이다. ### 4-2. egress gateway 경유 (HTTP) [Egress gateway 매뉴얼](/docs/istio/egress/gateway/) §06(origination) 골격과 거의 같되, **외부 leg의 TLS origination(DestinationRule)만 빼면** 된다. 내부 leg(사이드카→게이트웨이)는 외부가 http든 https든 무관하게 ISTIO_MUTUAL로 감쌀 수 있다 — 이게 **"두 개의 독립 스위치"**(내부 hop 보안 vs 외부 hop 보안)의 실체다. ```yaml # ① ServiceEntry — 포트 80 HTTP 만 (443 HTTPS 불필요) apiVersion: networking.istio.io/v1 kind: ServiceEntry metadata: { name: partner-http, namespace: istio-system } spec: hosts: [api.partner.example.com] ports: - { number: 80, name: http, protocol: HTTP } resolution: DNS location: MESH_EXTERNAL --- # ② Gateway — 내부 leg 수신 (mTLS 불요 시 HTTP가 가장 단순) apiVersion: networking.istio.io/v1 kind: Gateway metadata: { name: egress-partner, namespace: istio-system } spec: selector: { istio: egressgateway } servers: - port: { number: 80, name: http, protocol: HTTP } hosts: [api.partner.example.com] # 내부 구간을 mTLS로 감싸려면: protocol: HTTPS + tls.mode: ISTIO_MUTUAL --- # ③ DestinationRule — subset 만 (외부 origination 없음 = 평문 그대로 나감) apiVersion: networking.istio.io/v1 kind: DestinationRule metadata: { name: egressgateway-partner, namespace: istio-system } spec: host: istio-egressgateway.istio-system.svc.cluster.local subsets: - name: partner # 내부 mTLS 명시: trafficPolicy.portLevelSettings[].tls.mode: ISTIO_MUTUAL --- # ④ VirtualService — http: 2-leg (tls/sniHosts 아님!) apiVersion: networking.istio.io/v1 kind: VirtualService metadata: { name: route-partner, namespace: istio-system } spec: hosts: [api.partner.example.com] gateways: [mesh, egress-partner] http: # ← tls: 아님 - match: [{ gateways: [mesh], port: 80 }] route: [{ destination: { host: istio-egressgateway.istio-system.svc.cluster.local, subset: partner, port: { number: 80 } } }] - match: [{ gateways: [egress-partner], port: 80 }] route: [{ destination: { host: api.partner.example.com, port: { number: 80 } } }] ``` §06(origination)과의 차이는 **공통 뼈대 + 델타 딱 둘**이다: (1) ServiceEntry에 443/HTTPS가 없고 **80/HTTP만**, (2) 외부 origination DestinationRule이 **없다**(외부로 새 TLS를 안 맺으니까). VirtualService도 `tls:`+`sniHosts`가 아니라 `http:` 2-leg인 점이 §2 척추 표 그대로다. --- ## 5. "동일하게 TLS로 해도 되나" — 의사결정 이 질문의 답은 §1에서 분리한 **외부 endpoint가 실제로 무엇을 받느냐에 100% 종속**된다. ``` 외부가 TLS(HTTPS)를 받아주는가? ├─ 예 → origination 가능. 앱 http면 SIMPLE(권장)/MUTUAL, 앱 https면 passthrough. │ 평문 노출이 없어 보안상 오히려 TLS 설정을 권장. └─ 아니오(HTTP만) → protocol: HTTP 외엔 선택지 없음. 외부 구간 평문 노출은 네트워크 계층(전용회선/VPN/사설 peering)으로 감싸야. ``` | 상황 | 권장 설정 | 비고 | |---|---|---| | 외부 HTTP만 받음 (질문 전제) | `protocol: HTTP` | TLS 설정은 깨짐. 평문 노출 불가피 | | 외부 HTTPS, 앱 https 유지 | passthrough (`protocol: TLS`) | 앱 변경 0, GW L7 0 | | 외부 HTTPS, 앱 http 가능 | **SIMPLE origination** | "앱 http인데 외부 https" — 평문 노출 제거 | | 외부 HTTPS + 파트너가 mTLS 요구 | MUTUAL origination | GW가 client cert 중앙관리 | > [!tip] > "TLS로 해도 되는지" = "외부가 TLS를 받아주는가"와 동치. 받아주면 origination이 가능하고 권장되며, 안 받아주면 `protocol: HTTP` 외엔 없다. --- ## 6. 적용 예시 — 의도대로 박혔는지 검증 설정이 §2 표의 어느 행으로 컴파일됐는지를 **Envoy config에서 직접** 확인한다(CR이 아니라 진실 쪽을 본다). 평문 HTTP가 목표일 때 기대값: ```bash # HTTP route가 생겼는지 — RDS istioctl proxy-config route deploy/myapp --name 80 -o json # 기대: virtualHosts[].domains 에 "api.partner.example.com" 이 있고 # routes[].route.cluster 가 "outbound|80||api.partner.example.com" (plain HTTP) # → cluster 이름이 outbound|80|| 로 시작하면 평문 HTTP route로 정상 박힌 것 # cluster가 HTTP로 잡혔는지 — CDS (plain HTTP면 transportSocket에 TLS 없어야 정상) istioctl proxy-config cluster deploy/myapp --fqdn api.partner.example.com -o json \ | grep -E '"name"|transportSocket' # 기대(평문): transportSocket 키 자체가 없음 → 평문 정상 # transportSocket(envoy.transport_sockets.tls)이 보이면 누군가 TLS originate 중 # origination이면: secret 로드 + transportSocket의 TLS 모드 확인 istioctl proxy-config secret deploy/istio-egressgateway -n istio-system # 기대: SDS 로 originate용 client cert/CA 가 로드(RESOURCE NAME 에 해당 secret) istioctl proxy-config cluster deploy/istio-egressgateway -n istio-system \ --fqdn api.partner.example.com -o json | grep -A5 transportSocket # 기대(origination): transportSocket 아래 "envoy.transport_sockets.tls" + sni 필드가 보임 ``` cluster 이름 `outbound|80||api.partner.example.com`은 `direction|port|subset|fqdn` 규칙대로다 — `direction=outbound`, `port=80`, `subset`(빈칸), `fqdn`. 평문이면 subset이 비고 transportSocket이 없는 게 정상이다. > [!note] > **가장 빠른 진단**: access log / 메트릭에 **method·path·status가 보이면** HTTP로 제대로 처리된 것이고, **SNI·바이트만 보이면** TLS/TCP로 잘못 등록한 것이다. plain HTTP인데 L7이 안 보이면 protocol을 의심하라. JSON 기준으로는 — route는 `cluster: outbound|80||...`(plain) 여부, cluster는 `transportSocket` 유무(없으면 평문, 있으면 TLS originate)가 결정적 단서다. --- ## 핵심 정리 > [!summary] > 1. egress에서 "https냐 http냐"는 **앱이 보내는 것**(우리가 바꿀 수 있음)과 **외부 endpoint가 받는 것**(상대가 정함)을 분리해야 풀린다. "TLS로 해도 되나"는 결국 **"외부가 TLS를 받아주는가"** 와 동치다. > 2. 외부 endpoint 프로토콜 = ServiceEntry `protocol` 이어야 한다. 외부가 평문 HTTP면 `protocol: HTTP`/`http:` 라우팅, 외부가 HTTPS면 `TLS`(passthrough) 또는 `HTTPS`(origination). 평문에 TLS 설정을 쓰면 핸드셰이크 실패(HTTPS 오등록)로 깨지거나, `ALLOW_ANY`에선 기존 catch-all 체인으로 조용히 새어나갈 수 있다(TLS 오등록 — §2 실측 정정 참고). > 3. 차이의 근원은 **port `protocol`이 Envoy listener 필터 체인을 고른다**는 것 — `HTTP`면 HTTP Connection Manager(L7 파싱), `TLS`면 tls_inspector+tcp_proxy(SNI만). **입력 첫 바이트 형태가 달라** 옵션이 아니라 체인이 안 맞물려 호환 불가다. > 4. 외부가 TLS를 받아주면 SIMPLE origination(앱 http→외부 https)이 가능하고 평문 노출을 없애 권장, 안 받아주면 `protocol: HTTP` 외엔 없고 외부 구간 평문은 네트워크 계층으로 감싸야 한다. > 5. plain HTTP egress의 유일한 보상은 **공짜 L7 가시성**(method/path/status, `istio_requests_total`). 검증은 route의 `cluster: outbound|80||...`와 cluster의 `transportSocket` 부재로 확인. --- ## What you might be missing - **평문 HTTP egress는 외부 구간이 통째로 암호화 안 된 채 나간다 — 보안/규제 핵심 리스크.** 내부 leg(사이드카↔게이트웨이)를 ISTIO_MUTUAL로 감싸도 그건 *내부 hop*일 뿐, **게이트웨이↔외부 endpoint 구간은 평문**이다. "내부 mTLS 걸었으니 안전"은 착각 — **바깥 봉투가 아예 없는** 상태다. 외부가 HTTPS를 지원하면 §3의 ③ SIMPLE origination으로 평문 구간을 없애라. - **포트 `name` 접두사도 프로토콜을 결정한다.** Istio는 `protocol`이 없거나 모호하면 port `name` 접두사(`http`/`http2`/`grpc`/`tls`/`tcp`)로 추론한다. ServiceEntry는 `protocol`이 우선이지만, 둘이 어긋나면(`name: tcp-foo` + `protocol: HTTP`) 혼란이 생기니 일치시켜라. - **평문 HTTP/2(h2c)·gRPC plaintext는 또 다르다.** `protocol: HTTP`는 HTTP/1.1. 외부가 평문 gRPC/h2c면 `protocol: HTTP2`(또는 `name: http2`/`grpc`)로 써야 ALPN 없이 HTTP/2 multiplexing이 된다 — HTTP/1.1로 잡으면 gRPC가 깨진다. - **`protocol: TCP`로 우회하면 동작은 하되 L7을 잃는다.** 평문 HTTP를 `protocol: TCP`(opaque)로 등록하면 tcp_proxy로 그냥 흐른다 — 연결은 되지만 method/path/status·`istio_requests_total`이 사라진다. plain HTTP의 거의 유일한 장점(공짜 L7 가시성)을 버리는 셈이라, 특별한 이유 없으면 `HTTP`로 둬라. - **`protocol: TLS` vs `HTTPS`(ServiceEntry)** — passthrough엔 `TLS`(SNI 기반 L4)가 표준이고, `HTTPS`는 주로 origination 외부 leg(게이트웨이가 종단/originate)에서 쓴다. 둘 다 "TLS 트래픽"이지만 Istio가 거는 처리가 미묘하게 달라, 공식 예제 관례(passthrough=TLS, origination 외부포트=HTTPS)를 따르는 게 안전하다. - **`protocol: TLS` 오등록은 "안전장치"가 아니다.** §2 실측(T17)에서 보듯, `ALLOW_ANY` 환경에서 평문 포트를 `TLS`로 잘못 선언해도 기존 catch-all HTTP 패스스루 체인이 옆에 남아 있어 트래픽이 조용히 성공한다 — "잘못 선언하면 바로 터지니 실수를 눈치챌 수 있다"는 기대는 환경에 따라 배신당할 수 있다. 오등록 탐지는 connection reset 여부가 아니라 `istioctl proxy-config listener`로 filterChain 구성을 직접 확인하는 쪽이 안전하다. --- ## See also - [Egress gateway 매뉴얼](/docs/istio/egress/gateway/) — passthrough/origination 전체 매뉴얼 (이 문서의 모태) - [Egress operations](/docs/istio/egress/operations/) — passthrough 운영·모니터링·graceful shutdown - [CR→xDS 멘탈 모델](/docs/istio/xds-envoy/cr-xds-model/) — protocol→필터 체인의 근거 - [Envoy response flags](/docs/istio/xds-envoy/envoy-response-flags/) — `NR`/reset 등 깨짐 시 응답 플래그 해석 --- > [!quote] 출처 (검증 기준 Istio 1.30 / `networking.istio.io/v1`) > - ServiceEntry `protocol`(HTTP/HTTPS/TLS/TCP/HTTP2/GRPC)별 sidecar 처리, port `name` 접두사 기반 프로토콜 추론, protocol sniffing — Istio *ServiceEntry* / *Protocol Selection* 공식 문서. > - passthrough(`protocol: TLS`+`PASSTHROUGH`+`sniHosts`) / TLS·mTLS origination(`SIMPLE`/`MUTUAL`+`credentialName`+`sni`) — Istio *Egress Gateways* / *Egress TLS Origination* 공식 문서. > - `protocol: HTTP` egress + `ALLOW_ANY` PassthroughCluster / `REGISTRY_ONLY` BlackHole, L7 telemetry 차이 — Istio *Accessing External Services* 가이드. > - 본 문서는 LLM 답변(2026-06-01 세션) 정리이며, 외부 endpoint HTTP/HTTPS 구분 관점은 [Egress gateway 매뉴얼](/docs/istio/egress/gateway/)의 TLS 모드 비교를 확장한 것. --- ## 관련 파일 · 참조 **이 문서의 정본(④ 평문 HTTP)** — §4 인라인 YAML 그대로의 스냅샷: - 📎 [serviceentry-partner-http.yaml](https://legacy.homelab89.com/istio/attachment/scenarios/20-egress/serviceentry-partner-http.yaml) — §4-1 사이드카 직접 egress (`protocol: HTTP` ServiceEntry 단독) - 📎 [egress-partner-http-gateway.yaml](https://legacy.homelab89.com/istio/attachment/scenarios/20-egress/egress-partner-http-gateway.yaml) — §4-2 egress gateway 경유 2-leg (ServiceEntry/Gateway/DestinationRule/VirtualService, origination 없음) **§3 HTTPS 참조용(passthrough — 모태 문서 정본)**: - 📎 [serviceentry-httpbin-ext.yaml](https://legacy.homelab89.com/istio/attachment/scenarios/20-egress/serviceentry-httpbin-ext.yaml) · 📎 [gateway-egress.yaml](https://legacy.homelab89.com/istio/attachment/scenarios/20-egress/gateway-egress.yaml) — 외부가 HTTPS인 passthrough(`protocol: TLS`) 예제. ④ 평문 HTTP가 아니라 ①을 보여주므로, §4를 읽는 중이라면 위의 `*-partner-http` 두 파일을 보라. - ↗ [Egress Gateway for HTTPS (SNI passthrough)](https://istio.io/latest/docs/tasks/traffic-management/egress/egress-gateway/#egress-gateway-for-https-traffic) · ↗ [Egress TLS Origination (HTTP→HTTPS upgrade)](https://istio.io/latest/docs/tasks/traffic-management/egress/egress-tls-origination/) > [!note] > 위 인라인 YAML과 스냅샷은 `networking.istio.io/v1`로 통일했다. 같은 트리의 passthrough 스냅샷 일부는 `v1beta1`로 선언돼 있는데, **Istio 1.30에서 두 apiVersion은 상호 호환**이라 동작은 동일하다 — 복붙 시 한쪽으로 맞추면 된다. **관련 검증** → [Egress mTLS 리포트 — HTTPS over mTLS (ISTIO_MUTUAL)](/docs/istio/egress/report-egress-mtls/) · [구조 정본 — CRD·장단점·활용·운영](/docs/istio/egress/https-over-mtls/) --- ## 검증 기록 (2026-07-05 · Istio 1.30.0 / k8s 1.30.6) 검증 방법: 공식 Istio/Envoy 문서(레퍼런스·블로그·아키텍처 문서) 대조 + homelab 클러스터(k8s 1.30.6, Istio 1.30.0) 실측 1건(T17, httpbin.org 대상 3-phase 재현). | 주장 | 판정 | 근거 | |---|---|---| | C1. ServiceEntry/Gateway 포트 `protocol`은 istiod가 거는 필터 체인을 고르는 스위치 | ✅ 문헌 확인 | [istio.io/.../protocol-selection](https://istio.io/latest/docs/ops/configuration/traffic-management/protocol-selection/) | | C2. `protocol: HTTP` → HCM, Host 헤더(RDS) 라우팅, `istio_requests_total` L7 메트릭 | ✅ 실측 확인 | [istio.io/.../metrics](https://istio.io/latest/docs/reference/config/metrics/) · [T17 실측](files/verify/T17/result.txt) | | C3. `protocol: TLS` → tls_inspector+tcp_proxy, SNI 라우팅, `istio_tcp_*`만(복호화 안 함) | ✅ 문헌 확인 | [.../service-entry](https://istio.io/latest/docs/reference/config/networking/service-entry/) · [tls_inspector](https://www.envoyproxy.io/docs/envoy/latest/configuration/listeners/listener_filters/tls_inspector) | | C4. `protocol: HTTPS`는 TLS 종단(복호화)→HCM이며 origination 외부 leg도 동일 방향 | ❌ 오류 — 본문 교정 | [Envoy TLS 아키텍처](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/security/ssl) (본문 §2 표+콜아웃으로 termination/origination 방향 정정) | | C5. 평문 HTTP를 `protocol: TLS`로 등록하면 SNI 추출 실패로 항상 connection reset | 🔬 실측 반증 — 본문 교정 | [tls_inspector](https://www.envoyproxy.io/docs/envoy/latest/configuration/listeners/listener_filters/tls_inspector) · [T17 실측](files/verify/T17/result.txt) (ALLOW_ANY에선 기존 catch-all 체인으로 성공 — §2 콜아웃 추가) | | C6. 평문 HTTP를 `protocol: HTTPS`로 등록하면 TLS 핸드셰이크 자체가 깨짐(반대 방향도 대칭) | ✅ 문헌 확인 | [Envoy TLS 아키텍처](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/security/ssl) | | C7. protocol sniffing이 있어도 명시된 `protocol`이 우선 | ✅ 문헌 확인 | [istio.io/.../protocol-selection](https://istio.io/latest/docs/ops/configuration/traffic-management/protocol-selection/) | | C8. `REGISTRY_ONLY`에서 ServiceEntry 없으면 BlackHoleCluster로 드롭 | ✅ 문헌 확인 | [istio.io/latest/blog/2019/monitoring-external-service-traffic](https://istio.io/latest/blog/2019/monitoring-external-service-traffic/) | | C9. `ALLOW_ANY`+ServiceEntry 미등록이면 `istio_requests_total` 같은 L7 메트릭이 안 잡힘 | 🔬 실측 반증 — 본문 교정 | [istio.io/latest/blog/2019/monitoring-external-service-traffic](https://istio.io/latest/blog/2019/monitoring-external-service-traffic/) · [T17 실측](files/verify/T17/result.txt) (PassthroughCluster로도 `istio_requests_total` 증가 확인 — §4-1 교정) | | C10. cluster 이름 `direction\|port\|subset\|fqdn` 규칙 | ✅ 문헌 확인 | [istio.io/.../proxy-cmd](https://istio.io/latest/docs/ops/diagnostic-tools/proxy-cmd/) | | C11. `protocol` 없거나 모호하면 port `name` 접두사로 추론 | ✅ 문헌 확인 | [istio.io/.../protocol-selection](https://istio.io/latest/docs/ops/configuration/traffic-management/protocol-selection/) | | C12. `protocol: HTTP`=HTTP/1.1, 평문 gRPC/h2c는 `HTTP2`로 선언 필요 | ✅ 문헌 확인 | [istio.io/.../protocol-selection](https://istio.io/latest/docs/ops/configuration/traffic-management/protocol-selection/) | | C13. `protocol: TCP`(opaque)로 등록하면 연결은 되나 L7(`istio_requests_total`)이 사라짐 | ✅ 문헌 확인 | [istio.io/.../metrics](https://istio.io/latest/docs/reference/config/metrics/) | | C14. Istio 1.30에서 `networking.istio.io/v1`과 `v1beta1`은 상호 호환 | ✅ 문헌 확인 | [istio.io/latest/blog/2024/v1-apis](https://istio.io/latest/blog/2024/v1-apis/) | | C15. MUTUAL origination은 client cert 제시, SIMPLE은 외부 server cert만 검증 | ✅ 문헌 확인 | [istio.io/.../egress-tls-origination](https://istio.io/latest/docs/tasks/traffic-management/egress/egress-tls-origination/) |