Istio는 SPIFFE 표준으로 워크로드 신원을 X.509 SVID의 SAN에 박아 mTLS 인증의 토대로 삼는다
Istio의 workload identity는 IP·hostname·header가 아니라 SPIFFE ID(spiffe://<trust-domain>/ns/<ns>/sa/<sa>)다. 이 ID는 X.509 인증서(SVID)의 SAN(URI type) 에 박혀 발급되고, mTLS handshake에서 양측이 서로의 SVID를 trust bundle(CA root)로 검증한다. 검증된 SPIFFE ID가 곧 source.principal로 이어져 인가(authz)의 입력이 된다. 즉 mTLS는 "암호화"가 아니라 검증 가능한 신원의 운반 수단이며, 그 신원이 없으면 principal 기반 정책 자체가 성립하지 않는다. 인증서는 istio-agent가 SDS로 메모리상에서 발급·로테이션하므로 디스크에 평문 key가 떨어지지 않는다.
이 note는 SPIFFE/SVID/SDS의 개념·멘탈모델을 정본으로 다룬다. 이 신원이 어떻게 정책 입력으로 평가되는가(authz 평가 순서, mTLS 유무별 가능 조건, egress passthrough)는 AuthorizationPolicy 멘탈모델을, 세 리소스(PeerAuthentication·RequestAuthentication·AuthorizationPolicy)의 역할 분담은 보안 리소스 trio를 본다.
01. 배경 — 왜 신원이 먼저인가 (IP/header/JWT를 못 믿는 이유)
분산 mesh에서 서버가 "이 요청을 보낸 client가 누구인가"를 판단할 근거는 빈약하다. 가진 후보를 하나씩 떨어뜨려 보면 왜 mTLS만 남는지 보인다.
IP? NAT·proxy·Pod 재생성으로 휘발. spoof 가능. L3 신원은 약함.
header? app 레벨에서 위조 가능. 누구나 X-User를 채울 수 있음.
JWT? end-user(사람) 신원엔 적합하나 service-to-service 신원과는 별개 축.
mTLS? sidecar가 workload 신원으로 발급받은 cert를 handshake에서 상호 검증.
서비스 간(service-to-service) 인증에는 암호학적으로 위조 불가능한 신원이 필요하다. mTLS의 X.509 cert는 CA 서명으로 위조를 막고, 그 cert 안에 신원 문자열을 박아 운반한다. 이 신원 문자열의 표준 포맷이 SPIFFE ID다.
이 배경에서 이 문서 전체를 관통하는 멘탈모델 한 줄이 나온다.
신원 발급 → mTLS로 검증 → 검증된 신원으로 인가 — 이 단방향 파이프라인이 척추다. 인가(authz)는 신원이 확립된 다음에야 의미를 갖는다. principal 기반 정책은 그 앞 단계인 mTLS가 신원을 운반해 줘야만 입력값이 생긴다.
이 파이프라인을 따라가며 채워 넣는다 — 신원이 무엇이고(§02 포맷, §03 SVID/SAN), 애초에 그 cert가 어떻게 손에 들어오며(§04 SDS 발급·로테이션), 어떻게 검증되고(§05 trust bundle, §06 secure naming), 이 모든 게 일반 TLS와 왜 다른가(§07).
02. SPIFFE ID 포맷과 Kubernetes 매핑
SPIFFE(Secure Production Identity Framework For Everyone)는 워크로드 신원의 이름 규칙을 정한 표준이다. Istio가 발급하는 SPIFFE ID는 URI다.
spiffe://<trust-domain>/ns/<namespace>/sa/<service-account>
예:
spiffe://cluster.local/ns/default/sa/productpage
spiffe://cluster.local/ns/payments/sa/payment-api
spiffe://cluster.local/ns/inference/sa/model-gateway
| 구성요소 | 출처 | 의미 |
|---|---|---|
trust-domain |
mesh 설정(기본 cluster.local) |
신뢰 경계. 같은 trust-domain끼리 root CA를 공유 |
ns/<namespace> |
Pod의 namespace | 격리 단위 |
sa/<service-account> |
Pod의 ServiceAccount | 신원의 실질 주체 |
핵심은 Istio 신원이 Pod·IP가 아니라 Kubernetes ServiceAccount에 1:1로 묶인다는 점이다. 같은 SA를 쓰는 Pod가 10개로 스케일아웃해도 신원은 하나다. 따라서 "신원을 분리하려면 ServiceAccount를 분리한다"가 운영 제1원칙이 된다 — Deployment마다 전용 SA를 주는 습관이 곧 최소권한 정책의 토대다.
AuthorizationPolicy의 source.principal에는 spiffe:// scheme을 떼고 cluster.local/ns/default/sa/productpage 형태로 쓴다. cert SAN에는 spiffe://... 전체가 들어가지만, 정책 비교 시점에는 scheme이 제거된 값이 principal이다(§01 파이프라인의 "scheme 제거" 화살표). 이 표기 불일치를 모르면 정책이 조용히 매칭 실패한다.
03. SVID — 신원을 어디에 박는가 (URI SAN)
신원을 운반하는 인증서를 SPIFFE에서는 SVID(SPIFFE Verifiable Identity Document)라 부른다. Istio는 X.509 형태의 SVID를 쓰며, SPIFFE ID는 cert의 Subject Alternative Name 중 URI 타입에 들어간다(CN이 아님 — CN/Subject는 비워두거나 무시).
Certificate:
Subject: (비어 있음 / 무의미)
X509v3 Subject Alternative Name:
URI:spiffe://cluster.local/ns/default/sa/productpage ← 여기가 신원
X509v3 Extended Key Usage:
TLS Web Server Authentication, TLS Web Client Authentication
(server·client 양쪽 = mutual TLS에 동시 사용)
SAN의 URI 타입을 쓰는 이유는, DNS-SAN(hostname)과 달리 신원이 네트워크 위치와 분리되기 때문이다. hostname은 어디 배포됐는지(where)를 말하지만 SPIFFE URI는 누구인지(who)를 말한다. 그리고 EKU에 server·client 인증이 둘 다 박히는 것이 mutual TLS의 형식적 전제 — 한 cert가 handshake에서 server 역할과 client 역할을 동시에 수행한다. 이 who/where 분리가 secure naming(§06)을 가능하게 한다.
04. istio-agent의 SDS — 그 cert가 어떻게 손에 들어오는가 (발급·로테이션)
신원 포맷을 알았으니, 그 cert를 어떻게 워크로드 손에 쥐여주는가가 SDS(Secret Discovery Service)다. Istio 1.30에서 cert는 디스크의 Secret 파일이 아니라, 각 Pod의 istio-agent가 메모리에서 SDS로 Envoy에 push한다. §01 파이프라인의 "발급" 박스를 펼친 것이다.
단계별 메커니즘(이 4단계가 신뢰 사슬의 전부다):
- private key는 Pod 안에서 생성. istio-agent가 key를 만들고, 그에 대한 CSR(certificate signing request)을 만든다. private key는 절대 Pod 밖으로 나가지 않는다 — 그래서 네트워크·CA를 털어도 key는 못 얻는다.
- 신원 증명은 SA token으로. agent는 Kubernetes가 마운트한 projected ServiceAccount token(audience
istio-ca)을 CSR과 함께 istiod에 보낸다. "내가 누구다"라는 주장의 근거다. - istiod CA가 토큰을 검증하고 서명. istiod는 그 SA token을 Kubernetes API의
TokenReview로 검증해 "이 요청자가 정말 그 namespace/SA의 워크로드인가"를 확인한 뒤, 거기서 도출한 SPIFFE ID를 SAN에 박아 cert를 서명·발급한다. 신원을 요청자 말이 아니라 K8s API가 보증하는 것이 핵심 — 주장을 K8s가 사실로 승격시킨다. - SDS로 Envoy에 push. agent가 받은 cert chain + private key + root trust bundle을 SDS API(gRPC, UDS
/var/run/secrets/workload-spiffe-uds/socket)로 Envoy에 내려준다. Envoy는 이를 메모리상dynamicActiveSecrets로 들고 있는다.
자동 로테이션
cert는 단명(short-lived, 기본 24h, grace ~ 50%) 하다. istio-agent가 만료 전에 위 1~4를 다시 돌려 새 cert를 받고, SDS로 push하면 Envoy가 connection 끊김 없이(hot reload) validation context와 cert를 교체한다.
기본 수명 24h
재발급 시점 수명의 약 50% 경과 시 (절반에서 미리 갱신)
교체 방식 SDS push → Envoy hot reload (재시작·재연결 불필요)
관측 포인트 istioctl proxy-config secret <pod> 의
"Valid Cert" 의 issue/expire 시각이 주기적으로 갱신됨
단명 cert의 의의: 키가 유출돼도 노출 창이 수 시간으로 제한된다. 또 디스크에 key가 없으니 Pod 파일시스템 탈취만으로는 신원을 훔칠 수 없다. SDS·로테이션의 data-plane 동기화 측면 detail은 data-plane sync 상태를, secret을 Envoy admin에서 직접 들여다보는 진단은 Envoy admin API 진단을 본다.
05. SVID를 trust bundle로 검증하는 절차 (두 층위)
발급된 SVID가 mTLS handshake에 올라오면, 받은 쪽은 그것을 신뢰할지 결정해야 한다 — §01 파이프라인의 "검증" 박스다. 신뢰의 근거는 cert가 공유된 root CA(trust bundle)로 서명되었는지다. trust bundle은 istio-agent가 SDS로 함께 받아(§04 4단계의 root) Envoy의 validation context에 채워둔다.
검증은 별개의 두 층위이고, 이 둘을 구분 못 하면 "왜 연결은 되는데 정책이 안 먹나"를 못 푼다.
[chain 검증] 받은 leaf SVID → (intermediate) → root CA 까지 서명 chain이
trust bundle의 root로 연결되고 유효기간·서명이 맞는가.
→ 표준 X.509 PKI 검증. 실패하면 handshake 자체가 깨짐.
[identity 검증] chain이 유효해도 "그래서 누구냐"는 SAN URI를 꺼내야 안다.
이 값이 secure naming(기대 SA)·authz principal과 대조된다.
여기서 자주 헷갈리는 점: handshake 성공 = 인가 통과가 아니다. handshake(=chain 검증)는 "상대가 trust-domain 안의 검증된 워크로드임"까지만 보장한다. "그 워크로드가 이 작업을 해도 되는가"는 그 다음 단계인 AuthorizationPolicy(RBAC filter)가 identity 검증으로 뽑은 SAN principal로 결정한다(인가 평가 순서 CUSTOM→DENY→ALLOW).
06. secure naming — client 쪽 신원 검증
§05의 identity 검증은 서버만 하는 게 아니다. client Envoy도 자신이 붙은 서버의 SVID를 검증한다 — 단순 chain 검증을 넘어, "내가 호출하려던 service에 기대되는 ServiceAccount가 서버 cert의 SAN과 일치하는가"를 본다. 이를 secure naming이라 한다.
client는 reviews.default.svc 를 호출하려 한다.
istiod는 client에게 "reviews service의 정당한 SA 목록"을 함께 내려준다.
서버 SVID의 SAN URI가 그 기대 SA와 다르면 → client가 연결 거부.
효과: DNS spoof·IP hijack·BGP 장난으로 트래픽이 엉뚱한 워크로드로 유도돼도, 그 워크로드의 cert SAN이 기대 신원과 다르면 client가 스스로 끊는다. 위치(어디로 연결됐나)가 아니라 신원(누구인가)으로 판단하기 때문에 가능한 방어다. 이것이 §03에서 URI-SAN(위치와 분리된 신원)을 쓰는 실익이다.
07. 일반 TLS와 무엇이 다른가
일반 TLS (단방향)
client → "너 진짜 그 server 맞아?" → server cert만 검증 → 암호화 채널.
서버는 client가 누구인지 모름. 신원은 한 방향.
Istio mTLS (양방향, ISTIO_MUTUAL)
양측이 한 handshake 안에서 서로의 SVID를 제시·검증.
서버도 client의 SPIFFE ID를 얻음 → authz의 source.principal 입력.
= 암호화 + 상호 신원 증명.
따라서 mTLS가 없는 구간(평문, 또는 일반 단방향 TLS·PASSTHROUGH)에서는 peer SVID가 없어 source.principal/namespace/serviceAccounts 기반 정책을 쓸 수 없고, source.ip/destination.port/connection.sni만 가능하다. 이 mTLS 유무별 가능 조건의 경계는 AuthorizationPolicy 멘탈모델 §04에 표로 정리돼 있다.
mTLS를 강제(평문 거부)하는 것은 이 신원 메커니즘이 아니라 PeerAuthentication STRICT의 몫이다. PERMISSIVE면 평문도 받아들여 peer cert가 없는 채로 들어오고, 그러면 principal 조건이 그냥 매칭 실패할 뿐 "평문이라서 거부"가 명시되는 게 아니다. 신원·인증·인가의 역할 분담은 보안 리소스 trio를 본다.
08. 예시 — 신원을 손으로 확인하고 검증하기
추상적 주장을 실제 cluster에서 떨어뜨려 확인하는 절차다. 모든 출력은 §01 파이프라인의 어느 박스를 보는 것인지 함께 표시한다.
(1) 발급된 secret이 Envoy 메모리에 있는가 — 발급 박스의 결과물 확인.
istioctl proxy-config secret <pod>.<ns>
# 기대 출력 (요약)
# RESOURCE NAME TYPE VALID CERT SERIAL NOT AFTER NOT BEFORE
# default Cert true ... 2026-06-08T..(약 24h) 2026-06-07T..
# ROOTCA CA true ... <장기, 보통 수년>
default가 leaf SVID(워크로드 신원), ROOTCA가 trust bundle(검증용 root)이다. default의 NOT AFTER가 약 24h 뒤이고 주기적으로 갱신되면 로테이션이 정상 동작 중이라는 신호다.
(2) 그 SVID의 SAN에 실제 SPIFFE ID가 박혔는가 — 신원이 cert 안 어디에 들어갔는지 확인(§03).
istioctl proxy-config secret <pod>.<ns> -o json \
| jq -r '.dynamicActiveSecrets[] | select(.name=="default") | .secret.tlsCertificate.certificateChain.inlineBytes' \
| base64 -d | openssl x509 -noout -text \
| grep -A1 'Subject Alternative Name'
# 기대 출력
# X509v3 Subject Alternative Name:
# URI:spiffe://cluster.local/ns/default/sa/bookinfo-productpage
CN/Subject가 아니라 URI-SAN에 spiffe://...가 박힌 것을 직접 본다. 이 값에서 spiffe://만 떼면 그대로 AuthorizationPolicy의 source.principal 비교값이 된다(§02 key 박스).
(3) 로테이션이 도는가 — 같은 명령을 시간차로 두 번 떠 NOT AFTER가 ~24h 뒤로 갱신되는지 본다. 갱신되면 §04 1~4단계가 hot reload로 반복되고 있다는 증거다.
핵심 정리
한 문장 멘탈모델: 신원(SPIFFE ID)을 SVID의 URI-SAN에 박아 발급하고, mTLS에서 trust bundle로 검증해 꺼낸 그 신원이 곧 authz의 입력이다 — 발급→검증→인가의 단방향 파이프라인.
- 신원 = SPIFFE ID
spiffe://<trust-domain>/ns/<ns>/sa/<sa>. IP/Pod가 아니라 Kubernetes ServiceAccount에 1:1. 신원 분리는 SA 분리로만. - 운반 = X.509 SVID의 URI-type SAN(CN 아님). SAN URI = who(신원), DNS-SAN = where(위치)와 분리 → secure naming의 토대.
- 발급 = istio-agent가 Pod 안에서 key 생성 → CSR + projected SA token(aud
istio-ca) → istiod CA가 TokenReview로 신원 보증 후 서명 → SDS로 Envoy push. key·신원이 디스크에 안 떨어짐. - 로테이션 = 단명 cert(기본 24h, ~50%에서 갱신) → SDS hot reload, 연결 끊김 없음. 유출 노출창 최소화.
- 검증 = handshake에서 양측이 상대 SVID를 trust bundle로 chain 검증(연결 성패) + SAN에서 peer SPIFFE ID 추출(identity). handshake 성공 ≠ 인가 통과.
- 연계 = 검증된 SPIFFE ID(scheme 떼고) = authz의 source.principal. mTLS 없으면 principal 정책 불가, ip/port/sni만 가능.
검증 cheat-sheet:
신원 확인 istioctl proxy-config secret <pod> → default(SVID)/ROOTCA(bundle)
SAN 확인 위 cert를 openssl x509 -text → URI:spiffe://...
로테이션 확인 secret의 NOT AFTER가 주기적으로 ~24h 뒤로 갱신
정책 표기 cert SAN은 spiffe:// 포함, principal은 scheme 떼고 비교
What you might be missing
- trust-domain이 다르면 신원이 안 통한다. SPIFFE ID는 trust-domain을 포함하므로, multi-cluster·mesh federation에서 trust-domain이 다르거나 root CA(trust bundle)를 공유하지 않으면 상대 SVID chain 검증부터 실패한다. 멀티 클러스터 mTLS는 root CA 공유(또는 SPIFFE bundle endpoint 교환)와 trust-domain alias 설정이 전제다.
source.principal표기에서spiffe://scheme을 붙이면 조용히 매칭 실패한다. cert SAN에는 scheme이 있지만 authz principal 비교값에는 없다. 정책이 "에러 없이 그냥 안 먹는" 흔한 원인.- 신원 분리는 ServiceAccount 분리로만 된다. Deployment 여러 개가 default SA를 공유하면 SPIFFE ID가 같아져 authz로 구별 불가. 워크로드별 전용 SA가 최소권한의 출발점이다. label은 selector(정책 부착 대상)를 정할 뿐 신원이 아니다.
- 단명 cert 로테이션은 control-plane 가용성에 의존한다. istiod CA가 장시간 다운이면 cert 재발급이 막혀, 기존 cert 만료 시점부터 mTLS handshake가 깨지기 시작한다(즉시는 아니고 수명-grace 이후). istiod HA·CA 가용성이 곧 data-plane 신원 가용성이라는 점은 control-plane 성능 요인과 함께 본다.
- SVID 발급의 신뢰 뿌리는 Kubernetes SA token의 무결성. istiod는 TokenReview로 신원을 보증하므로, SA token projection(audience
istio-ca)이 깨지거나 K8s API가 토큰 검증을 못 하면 발급 자체가 실패한다. "cert가 안 나온다"는 보통 token/RBAC 쪽 문제지 CA 자체 문제가 아닐 때가 많다. - handshake 성공을 인가로 착각하지 말 것. mTLS가 켜졌다고 접근통제가 되는 게 아니다. PeerAuthentication STRICT(평문 거부) + AuthorizationPolicy(principal 기반 ALLOW)가 함께 있어야 "검증된 신원 중 허용된 것만" 통과한다.