Istio 서비스 메시 · 보안 — mTLS 와 AuthorizationPolicy · 이론
제로 트러스트 메시 — 신원, 암호화, 그리고 인가
한 줄 요약
메시 보안은 세 층이다. 누구인가(SPIFFE 아이덴티티) → 암호화를 강제하는가(PeerAuthentication) → 그래서 무엇을 할 수 있는가(AuthorizationPolicy). 순서를 건너뛰면 반드시 사고가 난다.
왜 이게 필요했나
쿠버네티스 클러스터 안은 흔히 "신뢰할 수 있는 네트워크"로 취급되지만, 실제로는 파드 하나만 탈취당해도 클러스터의 모든 서비스에 평문으로 붙을 수 있는 평평한 네트워크인 경우가 많다. 이걸 애플리케이션에서 막으려면 서비스마다 TLS 인증서 발급·갱신·검증 로직과 권한 검사 로직을 넣어야 한다. 언어가 넷이면 구현도 넷이고, 넷의 동작은 미묘하게 다르다.
그리고 더 근본적인 문제가 있다. 네트워크 정책만으로는 "누가" 를 표현할 수 없다. IP 기반 통제는 파드가 재시작하면 무너지고, 같은 노드에 뜬 다른 파드로 위장하는 것도 막지 못한다. 메시는 신원의 기준을 IP 에서 쿠버네티스 서비스 어카운트로 옮기고, 그 신원을 인증서에 새겨 암호학적으로 검증한다.
어떻게 동작하나
아이덴티티. 형식은 SPIFFE 표준을 따른다.
spiffe://cluster.local/ns/mesh-lab/sa/payments ------------- -------- -------- 트러스트 도메인 네임스페이스 서비스 어카운트istiod 가 CA 를 겸한다. 파드가 뜨면 istio-agent 가 파드 안에서 키 쌍을 만들고 CSR 만 istiod 에 보낸다. 개인키는 파드 밖으로 나가지 않는다. istiod 는 서비스 어카운트 토큰을 검증해 CSR 의 SPIFFE ID 가 그 토큰의 네임스페이스/SA 와 맞는지 확인한 뒤 서명한다. 인증서 수명은 기본 24시간이고, 수명의 절반쯤에서 자동 갱신된다. 갱신된 인증서는 SDS 로 Envoy 에 전달되므로 파드 재시작도 커넥션 드레인도 필요 없다.
암호화 강제. PeerAuthentication 이 "들어오는 연결에 mTLS 를 요구할 것인가"를 정한다.
| 모드 | 동작 | 쓰는 때 |
| --- | --- | --- |
| PERMISSIVE | mTLS 와 평문 모두 수락 (기본값) | 마이그레이션 기간 |
| STRICT | mTLS 만 수락 | 목표 상태 |
| DISABLE | mTLS 끔 | 외부 TLS 종료 장비 뒤 등 예외 |
범위는 세 층이고 좁은 쪽이 이긴다 — 워크로드(selector 지정) > 네임스페이스(selector 없이 해당 네임스페이스) > 메시 전역(루트 네임스페이스인 istio-system 에 default 라는 이름). 예외가 필요하면 portLevelMtls 로 포트 하나만 연다.
여기서 방향을 헷갈리면 안 된다. PeerAuthentication 은 받는 쪽(서버, 인바운드) 설정이고, DestinationRule 의 trafficPolicy.tls 는 보내는 쪽(클라이언트, 아웃바운드) 설정이다. 서버가 STRICT 인데 클라이언트 쪽 DestinationRule 이 DISABLE 이면 연결이 통째로 실패한다(UF 플래그). 이 충돌은 istioctl analyze 가 잡아 준다. 클라이언트 쪽에서 메시가 발급한 인증서를 쓰겠다는 선언이 ISTIO_MUTUAL 이다.
최종 사용자 인증. RequestAuthentication 은 JWT 의 서명·발급자·수명을 검증한다. 여기서 가장 흔한 오해가 있다. 이 리소스만으로는 아무것도 막지 못한다. 규칙은 "토큰이 있다면 유효해야 한다"이고, 토큰이 없는 요청은 그냥 통과한다. 토큰을 필수로 만들려면 AuthorizationPolicy 로 requestPrincipals 를 요구해야 한다.
인가. 요청 하나마다 평가 순서가 정해져 있다.
1. CUSTOM → 외부 인가기(OPA 등)가 거부하면 즉시 거부2. DENY → 하나라도 매칭되면 즉시 거부3. ALLOW → 대상 워크로드에 ALLOW 정책이 하나도 없으면 허용(기본 개방) ALLOW 정책이 하나라도 있으면 매칭돼야 허용(기본 거부로 전환)마지막 줄이 핵심이다. ALLOW 정책이 하나 생기는 순간 그 워크로드는 화이트리스트 모드가 된다. 이 성질을 이용한 것이 spec: {} 관용구다 — ALLOW 정책은 존재하는데 매칭 규칙이 하나도 없으니 아무도 통과하지 못한다. 제로 트러스트의 출발점이 이 한 줄이다.
principals 에 적는 값은 SPIFFE ID 이지만 설정에서는 spiffe:// 접두어 없이 cluster.local/ns/mesh-lab/sa/frontend 로 적는다는 점을 기억해 두자. 그리고 principal 은 mTLS 인증서에서 나오므로, PERMISSIVE 상태에서 평문으로 들어온 요청은 principal 이 비어 있어 이 조건에 절대 매칭되지 않는다. STRICT 전환이 인가 설계의 전제 조건인 이유다.
현장에서 만나는 모습
첫째, 순서를 건너뛴 STRICT. 평문 출발지를 정리하지 않고 메시 전역 STRICT 를 켜면 사이드카 없는 크론, 레거시 VM, 커스텀 프로브가 한꺼번에 끊긴다. 정석은 관측(PERMISSIVE 유지하며 평문 비율 확인) → 평문 출발지 제거 → 비핵심 네임스페이스부터 STRICT → 메시 전역 STRICT 다. 완료 기준도 숫자로 정한다 — 예를 들어 "평문 요청이 7일간 0건".
둘째, 인가에서의 403. 자주 만나는 원인 셋은 (1) PERMISSIVE 라 principal 이 비어 매칭 실패, (2) 네임스페이스·서비스 어카운트 오타, (3) ALLOW 정책을 추가하는 순간 기본 거부로 바뀐다는 사실을 잊고 기존 경로를 열어 두지 않은 것이다.
셋째, 켜기 전에 그림자로 본다. AUDIT 액션과 dry-run 어노테이션은 트래픽을 막지 않고 "켰다면 무엇이 거부됐을지"만 기록한다. 기본 거부를 운영에 넣기 전에 반드시 이 단계를 거친다.
넷째, 워크로드마다 전용 서비스 어카운트. 여러 워크로드가 default SA 를 공유하면 SPIFFE ID 가 같아져 인가를 나눌 수 없다. 신원 설계가 곧 인가 설계다.
다음 실습에서 할 것
두 개의 실습이 이어진다. 앞의 실습에서는 PERMISSIVE 로 시작해 메시 전역 STRICT 를 깔고, 워크로드·포트 단위 예외를 만들고, 클라이언트 쪽 ISTIO_MUTUAL 을 설정한 뒤, STRICT 와 DISABLE 이 충돌할 때 정적 분석이 무엇을 말하는지 확인하고 단계적 전환 계획을 문서로 남긴다. 뒤의 실습에서는 빈 규칙 전면 거부에서 출발해 신원·메서드·경로·네임스페이스·조건으로 권한을 좁히고, DENY 안전망과 AUDIT 을 얹은 뒤 호출별 허용/거부 매트릭스를 만든다.
이 환경에서는 실제 mTLS 핸드셰이크가 일어나지 않는다. 대신 정책의 범위·우선순위·충돌을 매니페스트와 정적 분석으로 판정한다. 현업에서 나는 사고의 대부분도 핸드셰이크 실패가 아니라 정책 범위를 잘못 잡은 데서 나온다.