Egress 외부 endpoint 프로토콜별 Istio 설정 — HTTP vs HTTPS
외부로 나가는 트래픽의 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를 쓸 수 있고(권장), 안 받아주면 평문 외엔 선택지가 없다.
선행 개념: ServiceEntry(외부 서비스를 mesh 레지스트리에 등록), TLS origination(앱은 평문, Istio가 외부로 TLS를 새로 맺음), passthrough(앱의 TLS를 Istio가 안 건드리고 SNI로만 라우팅). 이 셋의 CRD 골격은 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 멘탈 모델의 원리 그대로다: 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 | TLS ClientHello | TLS 종단(복호화) → HTTP Connection Manager | 복호 후 Host 헤더 | istio_requests_total (주로 origination 외부 leg) |
이 표가 왜 호환 불가를 만드는지는 첫 바이트를 보면 즉시 드러난다. 평문 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에 위임한다.
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 매뉴얼이 정본이므로(passthrough §04, mTLS origination §06, TLS 모드 비교 §07) 여기서는 ④ 평문과 대비되는 차이점만 한 줄씩 남긴다.
- ① passthrough — 앱
https://직접, ServiceEntryprotocol: 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 하나면 끝이다.
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로 빠져istio_requests_total같은 L7 메트릭이 안 잡힌다 — ServiceEntry를 두는 실익이 바로 이 L7 가시성 + 라우팅/정책 대상화다.
4-2. egress gateway 경유 (HTTP)
Egress gateway 매뉴얼 §06(origination) 골격과 거의 같되, 외부 leg의 TLS origination(DestinationRule)만 빼면 된다. 내부 leg(사이드카→게이트웨이)는 외부가 http든 https든 무관하게 ISTIO_MUTUAL로 감쌀 수 있다 — 이게 "두 개의 독립 스위치"(내부 hop 보안 vs 외부 hop 보안)의 실체다.
# ① 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 중앙관리 |
"TLS로 해도 되는지" = "외부가 TLS를 받아주는가"와 동치. 받아주면 origination이 가능하고 권장되며, 안 받아주면 protocol: HTTP 외엔 없다.
6. 적용 예시 — 의도대로 박혔는지 검증
설정이 §2 표의 어느 행으로 컴파일됐는지를 Envoy config에서 직접 확인한다(CR이 아니라 진실 쪽을 본다). 평문 HTTP가 목표일 때 기대값:
# 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이 없는 게 정상이다.
가장 빠른 진단: access log / 메트릭에 method·path·status가 보이면 HTTP로 제대로 처리된 것이고, SNI·바이트만 보이면 TLS/TCP로 잘못 등록한 것이다. plain HTTP인데 L7이 안 보이면 protocol을 의심하라. JSON 기준으로는 — route는 cluster: outbound|80||...(plain) 여부, cluster는 transportSocket 유무(없으면 평문, 있으면 TLS originate)가 결정적 단서다.
핵심 정리
- egress에서 "https냐 http냐"는 앱이 보내는 것(우리가 바꿀 수 있음)과 외부 endpoint가 받는 것(상대가 정함)을 분리해야 풀린다. "TLS로 해도 되나"는 결국 "외부가 TLS를 받아주는가" 와 동치다.
- 외부 endpoint 프로토콜 = ServiceEntry
protocol이어야 한다. 외부가 평문 HTTP면protocol: HTTP/http:라우팅, 외부가 HTTPS면TLS(passthrough) 또는HTTPS(origination). 평문에 TLS 설정을 쓰면 connection reset/핸드셰이크 실패로 깨진다. - 차이의 근원은 port
protocol이 Envoy listener 필터 체인을 고른다는 것 —HTTP면 HTTP Connection Manager(L7 파싱),TLS면 tls_inspector+tcp_proxy(SNI만). 입력 첫 바이트 형태가 달라 옵션이 아니라 체인이 안 맞물려 호환 불가다. - 외부가 TLS를 받아주면 SIMPLE origination(앱 http→외부 https)이 가능하고 평문 노출을 없애 권장, 안 받아주면
protocol: HTTP외엔 없고 외부 구간 평문은 네트워크 계층으로 감싸야 한다. - 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이 없거나 모호하면 portname접두사(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: TLSvsHTTPS(ServiceEntry) — passthrough엔TLS(SNI 기반 L4)가 표준이고,HTTPS는 주로 origination 외부 leg(게이트웨이가 종단/originate)에서 쓴다. 둘 다 "TLS 트래픽"이지만 Istio가 거는 처리가 미묘하게 달라, 공식 예제 관례(passthrough=TLS, origination 외부포트=HTTPS)를 따르는 게 안전하다.
See also
- Egress gateway 매뉴얼 — passthrough/origination 전체 매뉴얼 (이 문서의 모태)
- Egress operations — passthrough 운영·모니터링·graceful shutdown
- CR→xDS 멘탈 모델 — protocol→필터 체인의 근거
- Envoy response flags —
NR/reset 등 깨짐 시 응답 플래그 해석
- ServiceEntry
protocol(HTTP/HTTPS/TLS/TCP/HTTP2/GRPC)별 sidecar 처리, portname접두사 기반 프로토콜 추론, 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: HTTPegress +ALLOW_ANYPassthroughCluster /REGISTRY_ONLYBlackHole, L7 telemetry 차이 — Istio Accessing External Services 가이드.- 본 문서는 LLM 답변(2026-06-01 세션) 정리이며, 외부 endpoint HTTP/HTTPS 구분 관점은 Egress gateway 매뉴얼의 TLS 모드 비교를 확장한 것.
관련 파일 · 참조
이 문서의 정본(④ 평문 HTTP) — §4 인라인 YAML 그대로의 스냅샷:
- 📎 serviceentry-partner-http.yaml — §4-1 사이드카 직접 egress (
protocol: HTTPServiceEntry 단독) - 📎 egress-partner-http-gateway.yaml — §4-2 egress gateway 경유 2-leg (ServiceEntry/Gateway/DestinationRule/VirtualService, origination 없음)
§3 HTTPS 참조용(passthrough — 모태 문서 정본):
- 📎 serviceentry-httpbin-ext.yaml · 📎 gateway-egress.yaml — 외부가 HTTPS인 passthrough(
protocol: TLS) 예제. ④ 평문 HTTP가 아니라 ①을 보여주므로, §4를 읽는 중이라면 위의*-partner-http두 파일을 보라. - ↗ Egress Gateway for HTTPS (SNI passthrough) · ↗ Egress TLS Origination (HTTP→HTTPS upgrade)
위 인라인 YAML과 스냅샷은 networking.istio.io/v1로 통일했다. 같은 트리의 passthrough 스냅샷 일부는 v1beta1로 선언돼 있는데, Istio 1.30에서 두 apiVersion은 상호 호환이라 동작은 동일하다 — 복붙 시 한쪽으로 맞추면 된다.
관련 검증 → Egress mTLS 리포트 — HTTPS over mTLS (ISTIO_MUTUAL) · 구조 정본 — CRD·장단점·활용·운영