Istio 심화 — 왜 그렇게 흐르는가 · Gateway 의 TLS 모드가 게이트웨이 리스너가 되는 법 · 이론
TLS 를 어디서 끝낼지가 게이트웨이 체인의 모양을 정한다
한 줄 요약
Gateway 의 servers[].tls.mode 는 게이트웨이 Envoy 리스너의 필터 체인 모양을 정한다. SIMPLE·MUTUAL·OPTIONAL_MUTUAL·ISTIO_MUTUAL 은 체인에 DownstreamTlsContext 를 달아 게이트웨이가 TLS 를 끝내고 HCM 으로 HTTP 라우팅을 한다. PASSTHROUGH·AUTO_PASSTHROUGH 는 복호화하지 않고 SNI 만 엿봐 체인을 고른 뒤 tcp_proxy 로 바이트를 넘긴다. 둘을 가르는 가장 확실한 증거는 클라이언트가 받은 인증서가 누구 것인가다.
왜 이게 필요했나
인그레스 게이트웨이는 메시 바깥의 클라이언트가 처음 만나는 Envoy 다. 여기서 TLS 를 어디서 끝낼지는 보안과 운영의 타협이다. 게이트웨이에서 끝내면 인증서를 한곳에서 관리하고, 복호화된 HTTP 를 보고 경로·헤더로 라우팅하고, 재시도·타임아웃·인가를 걸 수 있다. 대신 평문이 게이트웨이에 한 번 드러난다. 끝까지 암호화가 필요한 서비스(자체 인증서로 클라이언트를 검증하는 결제 백엔드, TLS 를 직접 다루는 데이터베이스)는 게이트웨이가 손대지 않고 넘겨야 한다.
Istio 는 이 선택을 Gateway 의 한 필드로 내렸다. 문제는 증상이 서로 닮았다는 것이다. "인증서 이름이 맞지 않는다", "연결이 바로 끊긴다", "301 이 끝없이 돈다" 는 모두 TLS 가 어디서 끝나는지 모르면 엉뚱한 곳을 고치게 만든다. 모드마다 Envoy 에 무엇이 생기는지 알면 proxy-config listener 한 번으로 원인이 보인다.
어떻게 동작하나
Gateway 는 게이트웨이 파드에 포트와 인증서만 연다. 트래픽이 어디로 갈지는 그 Gateway 를 가리키는 VirtualService 가 정하므로 Gateway 하나만으로는 라우트가 비어 있다(AUTO_PASSTHROUGH 만 예외). 모드별로 게이트웨이 리스너에 생기는 것은 이렇다.
| 모드 | TLS 를 끝내는 곳 | 클라이언트 인증서 | Envoy 에 생기는 것 |
| --- | --- | --- | --- |
| SIMPLE | 게이트웨이 | 요구하지 않음 | 체인에 DownstreamTlsContext(서버 인증서) + HCM |
| MUTUAL | 게이트웨이 | 반드시 요구 | 위에 require_client_certificate: true + validation_context |
| OPTIONAL_MUTUAL | 게이트웨이 | 내면 검증, 안 내도 통과 | validation_context 만, 요구는 끔 |
| ISTIO_MUTUAL | 게이트웨이 | istiod 가 발급한 메시 인증서 | 워크로드 인증서·메시 루트를 SDS 로 받아 검증 |
| PASSTHROUGH | 업스트림 | 게이트웨이는 보지 않음 | tls_inspector + server_names 매치 + tcp_proxy |
| AUTO_PASSTHROUGH | 업스트림 | 게이트웨이는 보지 않음 | SNI 자체가 outbound_.<포트>_.<subset>_.<호스트> 꼴 클러스터 이름, VirtualService 불필요 |
인증서는 파일이 아니라 SDS 로 들어온다. credentialName: shop-cert 는 게이트웨이와 같은 네임스페이스의 Secret 을 가리키고, Envoy 설정에는 kubernetes://shop-cert 라는 SDS 이름으로 보인다. MUTUAL 의 검증용 CA 는 같은 Secret 의 ca.crt 나 shop-cert-cacert 에서 온다.
라우트 설정 이름에도 규칙이 있다. HTTPS 서버는 https.<포트>.<포트이름>.<Gateway이름>.<네임스페이스>(예: https.443.https.shop-gw.default)로 Gateway 마다 따로 생기고, 평문 HTTP 서버는 http.<포트> 하나로 같은 포트의 모든 Gateway 호스트가 합쳐진다. httpsRedirect: true 는 그 평문 서버의 virtual host 에 require_tls: ALL 한 줄이 되어, Envoy 가 라우트를 보기도 전에 301 로 https 를 가리킨다. Location 의 호스트는 요청의 Host 헤더 그대로라 포트가 붙어 있으면 그 포트까지 남는다.
PASSTHROUGH 에서 게이트웨이가 목적지를 고를 수 있는 이유는 TLS 의 첫 메시지(ClientHello)에 SNI 가 평문으로 실리기 때문이다. tls_inspector 가 그것만 읽고, 체인의 server_names 와 맞춰 본다. 맞는 체인이 없으면 연결은 그 자리에서 닫히고 listener.<주소>.no_filter_chain_match 가 오른다. Istio 가 실제로 만드는 SIMPLE 체인에도 server_names 가 붙는다 — 한 포트에서 호스트마다 다른 인증서를 고르기 위해서다.
현장에서 만나는 모습
PASSTHROUGH 인데 게이트웨이 인증서를 바꿔도 소용이 없다. 클라이언트가 보는 것은 처음부터 백엔드 인증서다. openssl s_client -servername 으로 받은 인증서의 발급자를 보면 바로 드러난다.
IP 로 붙거나 오래된 클라이언트만 연결이 끊긴다. SNI 를 보내지 않으면 PASSTHROUGH 체인은 고를 수가 없다. curl 은 핸드셰이크 실패(rc=35)로만 보여 주니 게이트웨이의 no_filter_chain_match 를 함께 본다.
MUTUAL 로 바꾸자 일부 클라이언트가 "연결이 재설정됐다" 고 한다. TLS 1.3 에서는 클라이언트가 핸드셰이크를 끝냈다고 생각한 뒤에 거절이 도착해 오류 모양이 제각각이다. Envoy 쪽 ssl.fail_verify_no_cert 가 오르면 인증서를 안 낸 것이다.
리다이렉트가 이상한 포트를 가리킨다. 로드밸런서가 Host 헤더에 포트를 붙여 넘기면 Location 에도 남는다. 그리고 httpsRedirect 를 켠 평문 서버로 LB 헬스체크가 들어오면 301 이라 비정상으로 판정될 수 있다.
공식 문서: [Gateway](https://istio.io/latest/docs/reference/config/networking/gateway/) · [Secure Gateways](https://istio.io/latest/docs/tasks/traffic-management/ingress/secure-ingress/) · [Envoy TLS](https://www.envoyproxy.io/docs/envoy/v1.38.3/api-v3/extensions/transport_sockets/tls/v3/tls.proto)
다음 실습에서 할 것
Gateway 를 쓰고 istioctl 이 SIMPLE 에 인증서를 요구하는 것을 본 뒤, 모드 다섯 가지를 Envoy 의 모양으로 옮겨 적는다. openssl 로 CA·게이트웨이·클라이언트·백엔드 인증서를 만들어 SIMPLE·MUTUAL·PASSTHROUGH 리스너를 차례로 띄우고, 클라이언트가 받은 인증서의 지문으로 TLS 가 어디서 끝났는지 증명한다. 마지막으로 SNI 가 맞지 않을 때와 httpsRedirect 의 301 을 확인한다.