LabHub
배우기 러닝패스 코스

Istio深化 — なぜそう流れるのか

三つの TLS モードを起動し、誰が証明書を提示するかを確かめる

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

Gateway 의 TLS 모드가 게이트웨이 Envoy 리스너의 무엇이 되는지 규칙으로 옮기고, SIMPLE·MUTUAL·PASSTHROUGH 와 httpsRedirect 를 등가 리스너로 띄워 클라이언트가 받은 인증서로 TLS 가 끝나는 곳을 증명한다.

왜 중요한가

게이트웨이의 인증서 장애는 대개 'TLS 가 어디서 끝나는가' 를 잘못 안 데서 온다. 게이트웨이 인증서를 고쳤는데 클라이언트는 백엔드 인증서를 보고 있거나, SNI 가 없어 체인을 못 고른 연결을 인증서 문제로 착각한다. 모드별로 Envoy 에 무엇이 생기는지 알면 리스너 설정 한 장과 통계 한 줄로 가를 수 있다.

단계

  1. /root/ist2-gw/gw.yaml 에 Gateway 를 쓰세요 — 이름 shop-gw, 네임스페이스 default, selectoristio: ingressgateway, 서버 둘: ① 포트 443·이름 https·프로토콜 HTTPS, hosts shop.example.com, tls.mode: SIMPLE, tls.credentialName: shop-cert ② 포트 80·이름 http·프로토콜 HTTP, hosts shop.example.com, tls.httpsRedirect: true. istioctl validate -f gw.yaml 의 출력과 종료 코드를 /root/ist2-gw/01-validate.txt 에 담으세요(마지막 줄 rc=). 그다음 credentialName 줄만 뺀 사본을 /root/ist2-gw/gw-nocred.yaml 로 만들어 같은 명령으로 검사하고 /root/ist2-gw/01-nocred.txt 에 담으세요.
  2. /root/ist2-gw/02-modes.txt 에 다섯 줄을 적으세요 — SIMPLE, MUTUAL, OPTIONAL_MUTUAL, PASSTHROUGH, ISTIO_MUTUAL 마다 <모드> terminates=<gateway|upstream> client_cert=<none|required|optional|mesh> filter=<hcm|tcp_proxy> needs_vs=<yes|no> 꼴로 한 줄씩(공백으로 구분). terminates 는 TLS 를 복호화하는 곳, client_cert게이트웨이가 클라이언트 인증서를 어떻게 다루는가(mesh 는 istiod 가 발급한 메시 인증서를 요구), filter 는 체인의 마지막 네트워크 필터, needs_vs 는 트래픽이 흐르려면 VirtualService 가 있어야 하는가입니다. 서버의 프로토콜은 HTTPS(PASSTHROUGH 는 TLS)라고 봅니다.
  3. /root/ist2-gw/certs 에 openssl 로 인증서를 만드세요 — CA(ca.crt·ca.key, 주체 /O=lab/CN=lab-ca)와 그 CA 가 서명한 게이트웨이 인증서(gateway.crt·gateway.key, 주체 /O=gateway/CN=shop.example.com, SAN DNS:shop.example.com). 그다음 /root/ist2-gw/gw-simple.yaml 에 Envoy 설정을 쓰세요 — 관리 포트 9988, 리스너 127.0.0.1:10088 의 체인에 DownstreamTlsContext(게이트웨이 인증서) + HCM, 라우트 설정 이름은 Istio 가 이 HTTPS 서버에 붙이는 이름 https.443.https.shop-gw.default, domains 에 shop.example.com, 클러스터 outbound|8117||shop.default.svc.cluster.local127.0.0.1:8117(평문 업스트림 upstream.py 8117 ok). 띄운 뒤 shop.example.com 로 요청해 /root/ist2-gw/03-simple.txt 에 네 줄을 적으세요 — code=(HTTP 코드), body=(응답 본문), seen_o=(클라이언트가 받은 인증서 주체의 O 값만), seen_sha256=(그 인증서의 SHA-256 지문, openssl x509 -fingerprint -sha256 출력의 = 뒤).
  4. 같은 CA 로 클라이언트 인증서 /root/ist2-gw/certs/client.crt·client.key(주체 /O=client/CN=client, SAN DNS:client)를 만드세요. /root/ist2-gw/gw-mutual.yamlgw-simple.yaml 과 같되 DownstreamTlsContextrequire_client_certificate: truevalidation_context.trusted_ca(/root/ist2-gw/certs/ca.crt)를 더한 것입니다. 띄운 뒤 /root/ist2-gw/04-mutual.txt 에 네 줄을 적으세요 — without_cert_code=·without_cert_rc=(클라이언트 인증서 없이 요청한 HTTP 코드와 curl 종료 코드), with_cert_code=(--cert·--key 로 요청한 HTTP 코드), fail_verify_no_cert=(통계 listener.127.0.0.1_10088.ssl.fail_verify_no_cert 의 값).
  5. 같은 CA 로 백엔드 인증서 /root/ist2-gw/certs/backend.crt·backend.key(주체 /O=backend/CN=shop.example.com, SAN DNS:shop.example.com)를 만들고, openssl s_server -accept 8111 -cert /root/ist2-gw/certs/backend.crt -key /root/ist2-gw/certs/backend.key -www -quiet 로 TLS 업스트림을 띄우세요. /root/ist2-gw/gw-pass.yaml 에 PASSTHROUGH 게이트웨이를 쓰세요 — 관리 포트 9988, 리스너 127.0.0.1:10088tls_inspector 리스너 필터, 체인은 filter_chain_match.server_names: ["shop.example.com"]tcp_proxy 하나(transport_socket 없음), 클러스터 outbound|8111||shop.default.svc.cluster.local127.0.0.1:8111. 띄운 뒤 shop.example.com 로 요청해 /root/ist2-gw/05-passthrough.txt 에 세 줄을 적으세요 — code=, seen_o=, seen_sha256=(3단계와 같은 방식).
  6. gw-pass.yaml 로 뜬 게이트웨이에 두 번 붙어 보세요 — ① SNI 를 other.example.com 으로(--resolve other.example.com:10088:127.0.0.1), ② SNI 없이 IP 로(https://127.0.0.1:10088/). 둘 다 --cacert /root/ist2-gw/certs/ca.crt 를 줍니다. /root/ist2-gw/06-sni.txt 에 다섯 줄을 적으세요 — sni=other.example.com, sni_rc=(①의 curl 종료 코드), no_sni_rc=(②의 종료 코드), stat=(체인을 못 고른 연결을 세는 리스너 통계의 전체 이름), count=(두 번 붙은 뒤 그 통계의 값).
  7. /root/ist2-gw/gw-redirect.yaml 에 1단계의 80 서버에 해당하는 평문 리스너를 쓰세요 — 관리 포트 9988, 리스너 127.0.0.1:10098(transport_socket 없음), HCM 의 라우트 설정 이름은 Istio 가 평문 80 서버에 붙이는 이름 http.80, virtual host 의 domains 에 shop.example.com, 그 virtual host 에 require_tls: ALL, 라우트는 outbound|8117||shop.default.svc.cluster.local127.0.0.1:8117. 띄운 뒤 curl -H 'Host: shop.example.com' localhost:10098/cart 의 결과를 /root/ist2-gw/07-redirect.txt 에 두 줄로 적으세요 — code=(HTTP 코드), location=(Location 헤더 값 그대로).
  8. /root/ist2-gw/08-report.md 에 다섯 줄을 적으세요 — simple_seen_o=(3단계에서 본 O), passthrough_seen_o=(5단계), mutual_without_cert_code=(4단계), sni_mismatch_rc=(6단계 ①의 종료 코드), redirect_code=(7단계). 그 아래 - 로 시작하는 설명을 네 줄 이상 적으세요.

참고

Gateway 두 서버를 쓰고 istioctl 이 SIMPLE 에 요구하는 것을 본다

/root/ist2-gw/gw.yaml 에 Gateway 를 쓰세요 — 이름 shop-gw, 네임스페이스 default, selectoristio: ingressgateway, 서버 둘: ① 포트 443·이름 https·프로토콜 HTTPS, hosts shop.example.com, tls.mode: SIMPLE, tls.credentialName: shop-cert ② 포트 80·이름 http·프로토콜 HTTP, hosts shop.example.com, tls.httpsRedirect: true. istioctl validate -f gw.yaml 의 출력과 종료 코드를 /root/ist2-gw/01-validate.txt 에 담으세요(마지막 줄 rc=). 그다음 credentialName 줄만 뺀 사본을 /root/ist2-gw/gw-nocred.yaml 로 만들어 같은 명령으로 검사하고 /root/ist2-gw/01-nocred.txt 에 담으세요.

Gateway 는 게이트웨이 파드의 Envoy 에 포트와 인증서만 여는 리소스입니다. 라우트는 VirtualService 가 붙어야 생깁니다. SIMPLE 은 '게이트웨이가 TLS 를 끝낸다' 는 뜻이라 서버 인증서가 반드시 있어야 하고, Istio 는 그것을 credentialName 이 가리키는 Secret 에서 받습니다. 그래서 그 줄을 빼면 클러스터 없이도 istioctl 이 거절합니다 — 거절 이유를 그대로 담으세요. 종료 코드는 명령 바로 뒤의 $? 입니다. sed '/credentialName/d' 로 한 줄만 뺄 수 있습니다.

TLS 모드 다섯 가지를 Envoy 쪽 모양으로 옮겨 적는다

/root/ist2-gw/02-modes.txt 에 다섯 줄을 적으세요 — SIMPLE, MUTUAL, OPTIONAL_MUTUAL, PASSTHROUGH, ISTIO_MUTUAL 마다 <모드> terminates=<gateway|upstream> client_cert=<none|required|optional|mesh> filter=<hcm|tcp_proxy> needs_vs=<yes|no> 꼴로 한 줄씩(공백으로 구분). terminates 는 TLS 를 복호화하는 곳, client_cert게이트웨이가 클라이언트 인증서를 어떻게 다루는가(mesh 는 istiod 가 발급한 메시 인증서를 요구), filter 는 체인의 마지막 네트워크 필터, needs_vs 는 트래픽이 흐르려면 VirtualService 가 있어야 하는가입니다. 서버의 프로토콜은 HTTPS(PASSTHROUGH 는 TLS)라고 봅니다.

갈림길은 하나입니다 — 게이트웨이 Envoy 의 체인에 DownstreamTlsContext 가 생기는가. 생기면 게이트웨이가 복호화하고, 복호화했으니 HTTP 를 읽어 HCM 으로 라우팅할 수 있습니다. 생기지 않으면 게이트웨이는 암호문밖에 못 보므로 SNI 로 체인만 고르고 바이트를 넘길 수밖에 없습니다. 클라이언트 인증서는 Envoy 의 require_client_certificatevalidation_context 두 값의 조합으로 생각하세요. 표본 줄의 AUTO_PASSTHROUGH 는 SNI 자체에 목적지가 적혀 있어 VirtualService 가 필요 없는 예외입니다.

SIMPLE — 게이트웨이가 TLS 를 끝내고 자기 인증서를 내민다

/root/ist2-gw/certs 에 openssl 로 인증서를 만드세요 — CA(ca.crt·ca.key, 주체 /O=lab/CN=lab-ca)와 그 CA 가 서명한 게이트웨이 인증서(gateway.crt·gateway.key, 주체 /O=gateway/CN=shop.example.com, SAN DNS:shop.example.com). 그다음 /root/ist2-gw/gw-simple.yaml 에 Envoy 설정을 쓰세요 — 관리 포트 9988, 리스너 127.0.0.1:10088 의 체인에 DownstreamTlsContext(게이트웨이 인증서) + HCM, 라우트 설정 이름은 Istio 가 이 HTTPS 서버에 붙이는 이름 https.443.https.shop-gw.default, domains 에 shop.example.com, 클러스터 outbound|8117||shop.default.svc.cluster.local127.0.0.1:8117(평문 업스트림 upstream.py 8117 ok). 띄운 뒤 shop.example.com 로 요청해 /root/ist2-gw/03-simple.txt 에 네 줄을 적으세요 — code=(HTTP 코드), body=(응답 본문), seen_o=(클라이언트가 받은 인증서 주체의 O 값만), seen_sha256=(그 인증서의 SHA-256 지문, openssl x509 -fingerprint -sha256 출력의 = 뒤).

SIMPLE 서버 하나는 Envoy 에서 체인 하나에 transport_socket 하나입니다. Istio 는 인증서를 파일이 아니라 SDS(kubernetes://shop-cert)로 넣지만, 파일로 넣어도 Envoy 가 보는 것은 같습니다. 이름으로 붙으려면 curl --resolve 호스트:포트:127.0.0.1 --cacert 를 쓰고, 클라이언트가 실제로 받은 인증서는 openssl s_client -connect … -servername … 의 출력을 openssl x509 로 넘겨 보세요. SAN 을 인증서에 옮기려면 서명할 때 -copy_extensions copyall 이 필요합니다. 라우트 이름 규칙은 https.<포트>.<포트이름>.<Gateway이름>.<네임스페이스> 입니다.

MUTUAL — 같은 체인에 클라이언트 인증서 요구가 더해진다

같은 CA 로 클라이언트 인증서 /root/ist2-gw/certs/client.crt·client.key(주체 /O=client/CN=client, SAN DNS:client)를 만드세요. /root/ist2-gw/gw-mutual.yamlgw-simple.yaml 과 같되 DownstreamTlsContextrequire_client_certificate: truevalidation_context.trusted_ca(/root/ist2-gw/certs/ca.crt)를 더한 것입니다. 띄운 뒤 /root/ist2-gw/04-mutual.txt 에 네 줄을 적으세요 — without_cert_code=·without_cert_rc=(클라이언트 인증서 없이 요청한 HTTP 코드와 curl 종료 코드), with_cert_code=(--cert·--key 로 요청한 HTTP 코드), fail_verify_no_cert=(통계 listener.127.0.0.1_10088.ssl.fail_verify_no_cert 의 값).

MUTUAL 은 SIMPLE 위에 두 값을 얹은 것입니다 — '인증서를 반드시 내라'(require_client_certificate)와 '무엇으로 검증하라'(validation_context). Istio 에서는 검증용 CA 가 credentialName Secret 의 ca.crt(또는 <이름>-cacert)에서 옵니다. 인증서 없이 붙으면 HTTP 까지 가지 못하므로 코드는 응답이 없다는 뜻의 값이 되고, 종료 코드는 TLS 버전에 따라 다릅니다 — 그대로 적으세요. 거절은 Envoy 쪽 통계에 남습니다(/statsgrep).

PASSTHROUGH — 게이트웨이는 SNI 만 읽고 백엔드 인증서가 보인다

같은 CA 로 백엔드 인증서 /root/ist2-gw/certs/backend.crt·backend.key(주체 /O=backend/CN=shop.example.com, SAN DNS:shop.example.com)를 만들고, openssl s_server -accept 8111 -cert /root/ist2-gw/certs/backend.crt -key /root/ist2-gw/certs/backend.key -www -quiet 로 TLS 업스트림을 띄우세요. /root/ist2-gw/gw-pass.yaml 에 PASSTHROUGH 게이트웨이를 쓰세요 — 관리 포트 9988, 리스너 127.0.0.1:10088tls_inspector 리스너 필터, 체인은 filter_chain_match.server_names: ["shop.example.com"]tcp_proxy 하나(transport_socket 없음), 클러스터 outbound|8111||shop.default.svc.cluster.local127.0.0.1:8111. 띄운 뒤 shop.example.com 로 요청해 /root/ist2-gw/05-passthrough.txt 에 세 줄을 적으세요 — code=, seen_o=, seen_sha256=(3단계와 같은 방식).

PASSTHROUGH 에서 게이트웨이는 인증서도 키도 갖지 않습니다. 그래도 목적지를 골라야 하니 TLS 첫 메시지(ClientHello)에 평문으로 실린 SNI 만 tls_inspector 로 엿보고, 그 이름을 server_names 로 매치해 체인을 고릅니다. 복호화하지 않았으니 HCM 은 쓸 수 없고 tcp_proxy 가 바이트를 그대로 넘깁니다. 그래서 핸드셰이크 상대는 백엔드이고, 클라이언트가 받는 인증서도 백엔드 것입니다 — 3단계 결과와 지문을 비교해 보세요. s_server -www 는 HTTP 요청에 200 과 상태 페이지로 답합니다.

SNI 가 맞지 않거나 없으면 고를 체인이 없다

gw-pass.yaml 로 뜬 게이트웨이에 두 번 붙어 보세요 — ① SNI 를 other.example.com 으로(--resolve other.example.com:10088:127.0.0.1), ② SNI 없이 IP 로(https://127.0.0.1:10088/). 둘 다 --cacert /root/ist2-gw/certs/ca.crt 를 줍니다. /root/ist2-gw/06-sni.txt 에 다섯 줄을 적으세요 — sni=other.example.com, sni_rc=(①의 curl 종료 코드), no_sni_rc=(②의 종료 코드), stat=(체인을 못 고른 연결을 세는 리스너 통계의 전체 이름), count=(두 번 붙은 뒤 그 통계의 값).

PASSTHROUGH 리스너에는 server_names 가 붙은 체인 하나뿐이고 기본 체인이 없습니다. 이름이 다르거나, IP 로 붙어 curl 이 SNI 를 아예 보내지 않으면 Envoy 는 고를 체인이 없어 연결을 곧바로 닫습니다 — curl 에게는 핸드셰이크 실패로 보입니다. 리스너 통계의 이름은 listener.<주소>_<포트>. 로 시작합니다. /stats 에서 filter_chain 으로 찾아보세요.

httpsRedirect 는 virtual host 의 require_tls 한 줄이다

/root/ist2-gw/gw-redirect.yaml 에 1단계의 80 서버에 해당하는 평문 리스너를 쓰세요 — 관리 포트 9988, 리스너 127.0.0.1:10098(transport_socket 없음), HCM 의 라우트 설정 이름은 Istio 가 평문 80 서버에 붙이는 이름 http.80, virtual host 의 domains 에 shop.example.com, 그 virtual host 에 require_tls: ALL, 라우트는 outbound|8117||shop.default.svc.cluster.local127.0.0.1:8117. 띄운 뒤 curl -H 'Host: shop.example.com' localhost:10098/cart 의 결과를 /root/ist2-gw/07-redirect.txt 에 두 줄로 적으세요 — code=(HTTP 코드), location=(Location 헤더 값 그대로).

Istio 는 httpsRedirect: true 인 서버의 virtual host 에 require_tls: ALL 을 넣습니다. 그러면 Envoy 는 라우트를 보기도 전에 평문 요청을 https 로 돌려보냅니다 — 업스트림까지 가지 않습니다. 평문 HTTP 서버의 라우트 이름은 게이트웨이 이름 없이 http.<포트> 라서, 같은 포트를 쓰는 여러 Gateway 의 호스트가 한 라우트 설정에 합쳐집니다. 헤더는 curl -s -D - -o /dev/null 로 보세요. Location 의 호스트는 요청의 Host 헤더에서 옵니다.

모드별로 누가 TLS 를 끝내는지 정리한다

/root/ist2-gw/08-report.md 에 다섯 줄을 적으세요 — simple_seen_o=(3단계에서 본 O), passthrough_seen_o=(5단계), mutual_without_cert_code=(4단계), sni_mismatch_rc=(6단계 ①의 종료 코드), redirect_code=(7단계). 그 아래 - 로 시작하는 설명을 네 줄 이상 적으세요.

값은 앞 단계 파일에서 옮기세요. 설명 줄에는 '이 모드에서 게이트웨이 Envoy 체인에 무엇이 생기고, 그래서 클라이언트에게 무엇이 보이는가' 를 짝지어 적으면 운영에서 인증서 문제를 만났을 때 어디부터 볼지가 정리됩니다.