Istio 심화 — 왜 그렇게 흐르는가 · PeerAuthentication 이 Envoy 의 무엇이 되는가 · 讲解
평문을 막는 것은 필터 체인이 하나뿐이라서다
한 줄 요약
PeerAuthentication 은 받는 쪽 사이드카의 들어오는 리스너를 바꾼다. STRICT 는 TLS 로 판정된 연결만 받는 필터 체인 하나(클라이언트 인증서 필수, 메시 CA 로 검증)가 되고, PERMISSIVE 는 거기에 매치 없는 평문 체인이 하나 더 붙은 것이다. 인증서 SAN 의 SPIFFE ID 가 신원이고, 앞의 spiffe:// 를 뗀 문자열이 AuthorizationPolicy 의 principal 이 된다.
왜 이게 필요했나
쿠버네티스 네트워크는 기본적으로 평문이고, 파드 IP 는 신원이 아니다. 파드가 다시 뜨면 IP 가 바뀌고, 같은 IP 를 다른 워크로드가 물려받는다. 그래서 "orders 만 reviews 를 부를 수 있다" 를 IP 로 적으면 금방 틀린다. Istio 는 신원을 서비스 계정에 묶었다. istiod 가 파드의 서비스 계정 토큰을 확인한 뒤 spiffe://cluster.local/ns/default/sa/orders 같은 URI 를 SAN 에 넣은 인증서를 서명해 주고, 사이드카끼리는 이 인증서로 서로를 확인하는 mTLS 로 말한다.
문제는 메시가 한 번에 만들어지지 않는다는 것이다. 사이드카는 네임스페이스마다, 배포마다 차례로 들어간다. 그 도중에 받는 쪽이 평문을 거절하면 아직 사이드카가 없는 파드의 호출이 전부 끊긴다. 그래서 기본값은 PERMISSIVE 다 — mTLS 도 평문도 받는다. 부르는 쪽 사이드카는 상대에게 사이드카가 있으면 알아서 mTLS 로 보내므로(auto mTLS), 주입이 끝난 뒤 STRICT 로 조이면 된다. PeerAuthentication 은 그 조임을 적는 리소스다.
어떻게 동작하나
istiod 는 PeerAuthentication 을 받는 쪽 사이드카의 virtualInbound(15006) 리스너로 번역한다. 핵심은 리스너 필터 tls_inspector 다. 연결의 첫 바이트를 보고 TLS ClientHello 면 transport_protocol 을 tls 로 표시하고, Envoy 는 그 표시로 필터 체인을 고른다.
| PeerAuthentication | Envoy 의 inbound 리스너 |
| --- | --- |
| STRICT | transport_protocol: tls 체인 하나. require_client_certificate: true, trusted_ca 는 메시 루트 |
| PERMISSIVE(기본) | 위 체인 + 매치 조건 없는 평문 체인 |
| DISABLE | 평문 체인만 |
| portLevelMtls | 그 포트를 destination_port 로 거는 체인이 따로 생긴다 |
STRICT 에서 평문 연결은 맞는 체인이 없어 Envoy 가 HTTP 응답도 없이 닫는다. 클라이언트에게는 connection reset 으로 보이고, Envoy 에는 no_filter_chain_match 통계만 남는다. portLevelMtls 의 번호는 Service 의 port 가 아니라 컨테이너가 듣는 워크로드 포트다. iptables 가 원래 목적지 포트를 보존한 채 15006 으로 꺾기 때문에 체인이 보는 번호가 그것이다. portLevelMtls 는 selector 가 있는 정책에서만 받아 준다.
신원 확인은 두 겹이다. 서명 검증은 "메시 CA 가 서명했는가" 만 본다. 같은 CA 에서 나온 인증서는 누구 것이든 통과한다. "누구인가" 는 SAN 이 답한다. Istio 는 SAN 대조를 주로 부르는 쪽에 건다 — 클라이언트가 서버 인증서의 SPIFFE ID 를 서비스가 기대하는 계정과 대조하는 secure naming 이다. 받는 쪽에서 "누가 부를 수 있는가" 는 AuthorizationPolicy 가 맡는다. mTLS 가 끝나면 Envoy 는 상대 인증서의 URI SAN 을 연결의 principal 로 기억하고, source.principals: ["cluster.local/ns/default/sa/orders"] 는 그것과 대조된다. 표기에서 스킴만 뺐을 뿐 같은 값이다.
현장에서 만나는 모습
STRICT 로 바꾸자 일부 호출만 connection reset by peer. 부르는 쪽에 사이드카가 없는 파드다(크론잡, 사이드카를 빼 둔 배치, 메시 밖의 모니터링). 받는 쪽 로그에는 요청이 아예 없다 — 체인을 고르는 단계에서 끊겼기 때문이다. no_filter_chain_match 가 늘고 있으면 이 경우다.
헬스 체크나 메트릭 포트만 평문으로 남기고 싶다. portLevelMtls 로 그 포트만 DISABLE 한다. 이때 Service 포트 번호를 적으면 아무 체인에도 걸리지 않아 조용히 무시된다. targetPort 를 적는다.
AuthorizationPolicy 를 걸었더니 전부 403. principals 에 spiffe:// 를 붙였거나, 신뢰 도메인이 다르거나(cluster.local 이 아닌 설치), 호출이 평문으로 들어와 principal 이 비어 있는 경우다. 셋 다 istioctl validate 는 통과한다.
공식 문서: [PeerAuthentication](https://istio.io/latest/docs/reference/config/security/peer_authentication/) · [Security concepts](https://istio.io/latest/docs/concepts/security/) · [AuthorizationPolicy](https://istio.io/latest/docs/reference/config/security/authorization-policy/) · [Envoy TLS](https://www.envoyproxy.io/docs/envoy/v1.38.3/api-v3/extensions/transport_sockets/tls/v3/tls.proto)
다음 실습에서 할 것
STRICT PeerAuthentication 을 쓰고 거른 뒤, openssl 로 메시 CA 와 SPIFFE 인증서 세 장을 만든다. 그 인증서로 STRICT·SAN 매처·PERMISSIVE·포트 단위 예외를 Envoy 체인으로 손수 세워 평문과 mTLS 가 각각 어떻게 되는지 확인하고, SAN 에서 AuthorizationPolicy 의 principal 을 끌어낸다.