LabHub
배우기 러닝패스 코스

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

SPIFFE 証明書を作り STRICT・PERMISSIVE のチェーンを組む

LabHub 에서 이어서 보기

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

목표

PeerAuthentication 을 쓰고, openssl 로 메시 CA 와 SPIFFE 인증서를 만들고, 각 모드가 되는 Envoy 필터 체인을 손으로 세워 평문과 mTLS 가 어떻게 갈리는지 확인한 뒤, 인증서의 SAN 에서 AuthorizationPolicy 의 principal 을 끌어낸다.

왜 중요한가

STRICT 로 바꾼 날 끊기는 호출, 포트 하나만 평문으로 남기려다 조용히 무시되는 정책, principal 을 잘못 적어 모두가 막히는 인가 정책 — 셋 다 istioctl validate 는 통과한다. 모드가 Envoy 의 어떤 체인이 되고 신원이 인증서의 어디에서 오는지 알면 통계 한 줄과 인증서 한 장으로 원인을 짚을 수 있다.

단계

  1. /root/ist2-mtls/pa.yaml 에 PeerAuthentication 을 쓰세요 — apiVersion: security.istio.io/v1, 이름 reviews-strict, 네임스페이스 default, selector.matchLabels.app: reviews, mtls.mode: STRICT. istioctl validate -f pa.yaml 의 출력과 종료 코드를 /root/ist2-mtls/01-validate.txt 에 담으세요(마지막 줄 rc=).
  2. /root/ist2-mtls 에서 openssl 로 자체 서명 CA 한 장(ca.crt·ca.key)을 만들고, 그 CA 로 서명한 인증서 세 장을 만드세요 — reviews.crt/reviews.key(서버), orders.crt/orders.key(허용할 클라이언트), intruder.crt/intruder.key(같은 CA 가 서명했지만 허용하지 않을 클라이언트). 각 인증서의 SAN 은 URI 하나, spiffe://cluster.local/ns/default/sa/<이름> 입니다. 그다음 인증서에서 실제로 읽은 SAN 을 /root/ist2-mtls/02-ids.txtreviews.crt=…, orders.crt=…, intruder.crt=… 세 줄로 적으세요.
  3. /root/ist2-mtls/strict.yaml 에 Envoy 설정을 쓰세요 — 관리 포트 9986, 리스너 virtualInbound127.0.0.1:10086 에서 듣고 리스너 필터로 envoy.filters.listener.tls_inspector 를 둡니다. 필터 체인은 하나, filter_chain_match.transport_protocol: tls 이고 DownstreamTlsContextrequire_client_certificate: true, 서버 인증서 /root/ist2-mtls/reviews.crt·/root/ist2-mtls/reviews.key, 신뢰 CA /root/ist2-mtls/ca.crt 를 줍니다(SAN 매처는 아직 넣지 않습니다). 그 체인은 모든 경로를 클러스터 inbound|8109||(127.0.0.1:8109)로 보냅니다. 업스트림을 8109ok 로 띄우고 Envoy 를 띄운 뒤 /root/ist2-mtls/03-strict.txt 에 네 줄을 적으세요 — plain=(평문 http://localhost:10086/strict 의 코드), mtls=(orders 인증서로 https://localhost:10086/strict 의 코드), mtls_body=(그 응답 본문), no_filter_chain_match=(통계 listener.127.0.0.1_10086.no_filter_chain_match 의 값).
  4. /root/ist2-mtls/strict.yaml/root/ist2-mtls/strict-san.yaml 로 복사한 뒤 그 사본에만 validation_contextmatch_typed_subject_alt_names 를 하나 더하세요 — san_type: URI, matcher.exact 는 2단계에서 읽은 orders.crt 의 SAN 그대로. 그 설정으로 Envoy 를 다시 띄우고 /root/ist2-mtls/04-san.txt 에 세 줄을 적으세요 — orders=(orders 인증서로 https://localhost:10086/san 의 코드), intruder=(intruder 인증서로 같은 요청의 코드), fail_verify_san=(통계 listener.127.0.0.1_10086.ssl.fail_verify_san 의 값).
  5. /root/ist2-mtls/strict.yaml 을 바탕으로 /root/ist2-mtls/permissive.yaml 을 만드세요 — 3단계의 TLS 체인은 그대로 두고, filter_chain_matchtransport_socket 도 없는 평문 체인을 하나 더합니다(같은 클러스터 inbound|8109|| 로). 그 설정으로 Envoy 를 다시 띄우고 /root/ist2-mtls/05-permissive.txt 에 세 줄을 적으세요 — plain=(평문 http://localhost:10086/permissive 의 코드), plain_body=(그 본문), mtls=(orders 인증서로 https://localhost:10086/permissive 의 코드).
  6. /root/ist2-mtls/pa-port.yaml 에 문서 두 개를 쓰세요 — (1) Service reviews(네임스페이스 default, selector app: reviews, port: 80targetPort: 10096), (2) PeerAuthentication reviews-port(같은 selector, mtls.mode: STRICT, portLevelMtls한 포트만 DISABLE). 그 포트 번호를 무엇으로 적어야 하는지는 힌트를 보세요. 그다음 /root/ist2-mtls/portlevel.yaml 에 Envoy 설정을 쓰세요 — 3단계와 같은 리스너(10086)에 additional_addresses127.0.0.1:10096 를 더 듣게 하고, filter_chain_match.destination_port: 10096 인 평문 체인 하나와 3단계의 TLS 체인 하나를 둡니다. Envoy 를 다시 띄우고 /root/ist2-mtls/06-portlevel.txt 에 네 줄을 적으세요 — validate_rc=(istioctl validate -f pa-port.yaml 의 종료 코드), plain_10096=, plain_10086=(각 포트로 평문 /port 의 코드), mtls_10086=(orders 인증서로 https://localhost:10086/port 의 코드).
  7. /root/ist2-mtls/orders.crt 의 SAN 을 openssl 로 읽어 /root/ist2-mtls/07-principal.txt 에 두 줄을 적으세요 — san=(읽은 URI 그대로), principal=(AuthorizationPolicy 의 principals 에 넣을 문자열). 그리고 /root/ist2-mtls/authz.yaml 에 AuthorizationPolicy 를 쓰세요 — apiVersion: security.istio.io/v1, 이름 reviews-from-orders, 네임스페이스 default, selector.matchLabels.app: reviews, action: ALLOW, 규칙 하나의 from[0].source.principals 에 그 principal 하나만. istioctl validate -f authz.yaml 이 통과해야 합니다.
  8. /root/ist2-mtls/08-report.mddefault_mode=(PeerAuthentication 이 하나도 없을 때의 모드), strict_plain=(3단계에서 평문 요청이 받은 코드), permissive_chains=(5단계 리스너의 필터 체인 수), orders_principal=(7단계의 principal) 네 줄을 적고, 그 아래 - 로 시작하는 설명을 네 줄 이상 적으세요.

참고

STRICT PeerAuthentication 을 쓰고 오프라인으로 거른다

/root/ist2-mtls/pa.yaml 에 PeerAuthentication 을 쓰세요 — apiVersion: security.istio.io/v1, 이름 reviews-strict, 네임스페이스 default, selector.matchLabels.app: reviews, mtls.mode: STRICT. istioctl validate -f pa.yaml 의 출력과 종료 코드를 /root/ist2-mtls/01-validate.txt 에 담으세요(마지막 줄 rc=).

PeerAuthentication 은 받는 쪽 워크로드의 설정입니다. reviews 를 부르는 쪽이 아니라 reviews 자신에게 '평문은 받지 말라' 고 거는 것이므로 selector 는 받는 쪽의 라벨이어야 합니다. selector 를 빼면 네임스페이스 전체, 루트 네임스페이스(istio-system)에 두면 메시 전체가 대상이 됩니다. istioctl validate 는 클러스터 없이 스키마만 봅니다 — 모드 값을 오타 내면 여기서 걸립니다.

메시 CA 와 SPIFFE 인증서 세 장을 만든다

/root/ist2-mtls 에서 openssl 로 자체 서명 CA 한 장(ca.crt·ca.key)을 만들고, 그 CA 로 서명한 인증서 세 장을 만드세요 — reviews.crt/reviews.key(서버), orders.crt/orders.key(허용할 클라이언트), intruder.crt/intruder.key(같은 CA 가 서명했지만 허용하지 않을 클라이언트). 각 인증서의 SAN 은 URI 하나, spiffe://cluster.local/ns/default/sa/<이름> 입니다. 그다음 인증서에서 실제로 읽은 SAN 을 /root/ist2-mtls/02-ids.txtreviews.crt=…, orders.crt=…, intruder.crt=… 세 줄로 적으세요.

Istio 의 신원은 이름(CN)이 아니라 SAN 의 URI 입니다 — spiffe://<신뢰 도메인>/ns/<네임스페이스>/sa/<서비스 계정>. istiod 는 파드의 서비스 계정 토큰을 확인한 뒤 바로 이 모양의 인증서를 서명해 줍니다. CSR 에 -addext "subjectAltName=URI:…" 로 SAN 을 넣어도, 서명할 때 openssl x509 -req-copy_extensions copyall 을 주지 않으면 확장이 버려집니다. 서명 뒤에 openssl x509 -noout -ext subjectAltName 으로 꼭 확인하고, openssl verify -CAfile ca.crt 로 사슬도 보세요.

STRICT 는 TLS 체인 하나뿐인 리스너가 된다

/root/ist2-mtls/strict.yaml 에 Envoy 설정을 쓰세요 — 관리 포트 9986, 리스너 virtualInbound127.0.0.1:10086 에서 듣고 리스너 필터로 envoy.filters.listener.tls_inspector 를 둡니다. 필터 체인은 하나, filter_chain_match.transport_protocol: tls 이고 DownstreamTlsContextrequire_client_certificate: true, 서버 인증서 /root/ist2-mtls/reviews.crt·/root/ist2-mtls/reviews.key, 신뢰 CA /root/ist2-mtls/ca.crt 를 줍니다(SAN 매처는 아직 넣지 않습니다). 그 체인은 모든 경로를 클러스터 inbound|8109||(127.0.0.1:8109)로 보냅니다. 업스트림을 8109ok 로 띄우고 Envoy 를 띄운 뒤 /root/ist2-mtls/03-strict.txt 에 네 줄을 적으세요 — plain=(평문 http://localhost:10086/strict 의 코드), mtls=(orders 인증서로 https://localhost:10086/strict 의 코드), mtls_body=(그 응답 본문), no_filter_chain_match=(통계 listener.127.0.0.1_10086.no_filter_chain_match 의 값).

istiod 가 STRICT 를 받으면 들어오는 리스너에서 평문 체인을 없앱니다. 남는 것은 tls_inspector 가 'TLS 다' 라고 판정한 연결만 받는 체인이고, 그 체인은 클라이언트 인증서를 요구해 메시 CA 로 검증합니다. 평문 연결은 맞는 체인이 없어 Envoy 가 그냥 닫습니다 — HTTP 응답조차 없으므로 curl 의 코드는 000 이고, 그 흔적이 no_filter_chain_match 통계입니다. 서버 인증서의 SAN 은 URI 뿐이라 curl 의 호스트 이름 검사가 통하지 않으니 -k 로 서버 확인만 끄고 --cert·--key 로 클라이언트 인증서는 내세요. (Istio 의 클라이언트는 호스트 이름 대신 서버 SAN 의 SPIFFE ID 를 확인합니다.)

같은 CA 가 서명해도 SAN 이 다르면 거절한다

/root/ist2-mtls/strict.yaml/root/ist2-mtls/strict-san.yaml 로 복사한 뒤 그 사본에만 validation_contextmatch_typed_subject_alt_names 를 하나 더하세요 — san_type: URI, matcher.exact 는 2단계에서 읽은 orders.crt 의 SAN 그대로. 그 설정으로 Envoy 를 다시 띄우고 /root/ist2-mtls/04-san.txt 에 세 줄을 적으세요 — orders=(orders 인증서로 https://localhost:10086/san 의 코드), intruder=(intruder 인증서로 같은 요청의 코드), fail_verify_san=(통계 listener.127.0.0.1_10086.ssl.fail_verify_san 의 값).

3단계의 체인은 '메시 CA 가 서명했는가' 만 봅니다. 그래서 같은 CA 에서 나온 intruder 도 통과했습니다. SAN 매처를 걸면 서명에 더해 '누구의 인증서인가' 까지 봅니다. 다만 Istio 가 이 매처를 쓰는 곳은 주로 클라이언트 쪽입니다 — 부르는 쪽이 '상대가 정말 reviews 의 신원인가' 를 확인하는 것(secure naming)이고, 받는 쪽에서 '누가 부를 수 있는가' 는 7단계의 AuthorizationPolicy 가 맡습니다. 여기서는 같은 원리를 받는 쪽에 걸어 Envoy 가 SAN 을 어떻게 대조하는지 봅니다. 거절은 TLS 핸드셰이크에서 일어나니 코드는 000 입니다.

PERMISSIVE 는 체인 두 개다

/root/ist2-mtls/strict.yaml 을 바탕으로 /root/ist2-mtls/permissive.yaml 을 만드세요 — 3단계의 TLS 체인은 그대로 두고, filter_chain_matchtransport_socket 도 없는 평문 체인을 하나 더합니다(같은 클러스터 inbound|8109|| 로). 그 설정으로 Envoy 를 다시 띄우고 /root/ist2-mtls/05-permissive.txt 에 세 줄을 적으세요 — plain=(평문 http://localhost:10086/permissive 의 코드), plain_body=(그 본문), mtls=(orders 인증서로 https://localhost:10086/permissive 의 코드).

PERMISSIVE 는 '둘 다 받는다' 입니다. Envoy 에서는 체인을 하나 더하는 것으로 표현됩니다. tls_inspector 가 첫 바이트를 보고 TLS 면 transport_protocol: tls 체인으로, 아니면 매치 조건이 없는 체인으로 보냅니다. Istio 가 실제로 만드는 체인에는 ALPN(istio-peer-exchange, istio) 조건도 붙지만 원리는 같습니다. 평문 체인에 transport_socket 을 붙이면 그 체인도 TLS 를 기다리게 되어 평문이 다시 막힙니다.

portLevelMtls 는 워크로드 포트에 거는 체인 하나다

/root/ist2-mtls/pa-port.yaml 에 문서 두 개를 쓰세요 — (1) Service reviews(네임스페이스 default, selector app: reviews, port: 80targetPort: 10096), (2) PeerAuthentication reviews-port(같은 selector, mtls.mode: STRICT, portLevelMtls한 포트만 DISABLE). 그 포트 번호를 무엇으로 적어야 하는지는 힌트를 보세요. 그다음 /root/ist2-mtls/portlevel.yaml 에 Envoy 설정을 쓰세요 — 3단계와 같은 리스너(10086)에 additional_addresses127.0.0.1:10096 를 더 듣게 하고, filter_chain_match.destination_port: 10096 인 평문 체인 하나와 3단계의 TLS 체인 하나를 둡니다. Envoy 를 다시 띄우고 /root/ist2-mtls/06-portlevel.txt 에 네 줄을 적으세요 — validate_rc=(istioctl validate -f pa-port.yaml 의 종료 코드), plain_10096=, plain_10086=(각 포트로 평문 /port 의 코드), mtls_10086=(orders 인증서로 https://localhost:10086/port 의 코드).

iptables 는 파드로 오는 연결을 15006 으로 꺾을 때 원래 목적지 포트를 보존합니다. 그 포트는 Service 의 port 가 아니라 컨테이너가 실제로 듣는 targetPort 이고, istiod 는 portLevelMtls 의 번호를 그대로 destination_port 로 거는 체인으로 만듭니다. 그래서 문서도 '워크로드 포트' 라고 적습니다. 또 portLevelMtls 는 selector 가 있는 정책에서만 받아 줍니다 — 빼 보면 istioctl validate 가 거절합니다. 이 파드에서는 iptables 대신 리스너 하나가 두 포트를 듣게 해 같은 모양을 만듭니다.

SAN 에서 spiffe:// 를 떼면 principal 이 된다

/root/ist2-mtls/orders.crt 의 SAN 을 openssl 로 읽어 /root/ist2-mtls/07-principal.txt 에 두 줄을 적으세요 — san=(읽은 URI 그대로), principal=(AuthorizationPolicy 의 principals 에 넣을 문자열). 그리고 /root/ist2-mtls/authz.yaml 에 AuthorizationPolicy 를 쓰세요 — apiVersion: security.istio.io/v1, 이름 reviews-from-orders, 네임스페이스 default, selector.matchLabels.app: reviews, action: ALLOW, 규칙 하나의 from[0].source.principals 에 그 principal 하나만. istioctl validate -f authz.yaml 이 통과해야 합니다.

mTLS 가 끝나면 받는 쪽 Envoy 는 상대 인증서의 URI SAN 을 연결의 신원으로 기억합니다. AuthorizationPolicy 의 source.principals 는 그 신원과 대조되는데, 표기에서 스킴(spiffe://)을 뗀 <신뢰 도메인>/ns/<네임스페이스>/sa/<서비스 계정> 꼴을 씁니다. 셸에서는 ${san#spiffe://} 로 뗄 수 있습니다. 주의 — 스킴을 붙인 채 적어도 istioctl validate 는 통과합니다. 그 정책은 아무와도 맞지 않아 ALLOW 정책이면 모두를 막습니다. 그리고 principal 은 mTLS 로 들어온 연결에만 생기므로 평문을 허용하는 PERMISSIVE 포트에서는 이 규칙에 걸리는 요청이 없습니다.

모드마다 Envoy 에 생기는 것을 한 장으로 정리한다

/root/ist2-mtls/08-report.mddefault_mode=(PeerAuthentication 이 하나도 없을 때의 모드), strict_plain=(3단계에서 평문 요청이 받은 코드), permissive_chains=(5단계 리스너의 필터 체인 수), orders_principal=(7단계의 principal) 네 줄을 적고, 그 아래 - 로 시작하는 설명을 네 줄 이상 적으세요.

값은 앞 단계 파일에서 옮기세요. 설명에는 '모드 → Envoy 에 생기는 것 → 운영에서 보이는 증상' 을 짝지어 적으면 좋습니다. 기본 모드가 왜 그 값인지는, 메시에 사이드카를 차례로 넣어 가는 도중을 떠올려 보세요.