Istio 심화 — 왜 그렇게 흐르는가 · Gateway 의 TLS 모드가 게이트웨이 리스너가 되는 법 · 实验
세 가지 TLS 모드를 띄워 누가 인증서를 내미는지 확인한다
목표
Gateway 의 TLS 모드가 게이트웨이 Envoy 리스너의 무엇이 되는지 규칙으로 옮기고, SIMPLE·MUTUAL·PASSTHROUGH 와 httpsRedirect 를 등가 리스너로 띄워 클라이언트가 받은 인증서로 TLS 가 끝나는 곳을 증명한다.
왜 중요한가
게이트웨이의 인증서 장애는 대개 'TLS 가 어디서 끝나는가' 를 잘못 안 데서 온다. 게이트웨이 인증서를 고쳤는데 클라이언트는 백엔드 인증서를 보고 있거나, SNI 가 없어 체인을 못 고른 연결을 인증서 문제로 착각한다. 모드별로 Envoy 에 무엇이 생기는지 알면 리스너 설정 한 장과 통계 한 줄로 가를 수 있다.
단계
1. /root/ist2-gw/gw.yaml 에 Gateway 를 쓰세요 — 이름 shop-gw, 네임스페이스 default, selector 는 istio: 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.local → 127.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.yaml 은 gw-simple.yaml 과 같되 DownstreamTlsContext 에 require_client_certificate: true 와 validation_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:10088 에 tls_inspector 리스너 필터, 체인은 filter_chain_match.server_names: ["shop.example.com"] 와 tcp_proxy 하나(transport_socket 없음), 클러스터 outbound|8111||shop.default.svc.cluster.local → 127.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.local → 127.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단계). 그 아래 - 로 시작하는 설명을 네 줄 이상 적으세요.
참고
- 이 파드에는 진짜 istiod 도 진짜 사이드카도 없습니다. 그래서
istioctl proxy-config로 실제 생성물을 볼 수 없고, 번역 규칙을 알고 손으로 등가 Envoy 설정을 만들어 동작을 확인합니다. 같은 규칙이 운영 클러스터의proxy-config출력에 그대로 보입니다. - 인증서는 모두
/root/ist2-gw/certs의 CA 하나로 서명합니다. CA 는 3단계에서 한 번만 만드세요 — 다시 만들면 앞서 서명한 인증서가 새 CA 로 검증되지 않습니다. SAN 을 옮기려면 서명할 때-copy_extensions copyall이 필요합니다. - 3·4·5단계의 리스너는 같은 포트 10088 을 씁니다. 한 번에 하나만 띄우고, 단계마다 설정 파일은 따로 둡니다.
- TLS 업스트림(
openssl s_server)도setsid --fork nohup … </dev/null로 셸에서 떼어 띄우고, 다시 띄울 때는pkill -x openssl로 정리합니다. - Envoy 를 띄울 때는
setsid --fork nohup envoy -c <파일> --log-level warn > <로그> 2>&1 </dev/null로 셸에서 완전히 떼어 놓으세요. 다시 띄우기 전에는pkill -x envoy로 정리합니다 (pkill -f 'envoy -c'는 그 문자열이 든 셸 자신까지 죽입니다). - 업스트림 흉내용 서버가 이미지에 있습니다:
python3 /opt/lab/envoy/upstream.py <포트> ok|fail|slow. 응답 본문은<모드>:<포트> <경로>입니다. - 설정을 고친 뒤에는 띄우기 전에
envoy --mode validate -c <파일>로 먼저 거르세요. 클러스터 이름에|가 들어가므로 YAML 에서는 반드시 따옴표로 감쌉니다.
8个步骤
- Gateway 두 서버를 쓰고 istioctl 이 SIMPLE 에 요구하는 것을 본다
- TLS 모드 다섯 가지를 Envoy 쪽 모양으로 옮겨 적는다
- SIMPLE — 게이트웨이가 TLS 를 끝내고 자기 인증서를 내민다
- MUTUAL — 같은 체인에 클라이언트 인증서 요구가 더해진다
- PASSTHROUGH — 게이트웨이는 SNI 만 읽고 백엔드 인증서가 보인다
- SNI 가 맞지 않거나 없으면 고를 체인이 없다
- httpsRedirect 는 virtual host 의 require_tls 한 줄이다
- 모드별로 누가 TLS 를 끝내는지 정리한다