--- title: Istio는 SPIFFE 표준으로 워크로드 신원을 X.509 SVID의 SAN에 박아 mTLS 인증의 토대로 삼는다 date: 2026-06-10 type: note domain: istio tags: [istio, mtls, spiffe, security] --- > [!abstract] 이 문서가 다루는 것 > Istio의 workload identity는 IP·hostname·header가 아니라 **SPIFFE ID**(`spiffe:///ns//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 멘탈모델](/public/istio/sec__src-authorizationpolicy-mental-model.html)을, 세 리소스(PeerAuthentication·RequestAuthentication·AuthorizationPolicy)의 역할 분담은 [보안 리소스 trio](/public/istio/sec__note-security-resource-trio.html)를 본다. ## 01. 배경 — 왜 신원이 먼저인가 (IP/header/JWT를 못 믿는 이유) 분산 mesh에서 서버가 "이 요청을 보낸 client가 누구인가"를 판단할 근거는 빈약하다. 가진 후보를 하나씩 떨어뜨려 보면 왜 mTLS만 남는지 보인다. ```text 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가 신원을 운반해 줘야만 입력값이 생긴다. ```mermaid flowchart LR subgraph Issue["발급 (per Pod, SDS)"] AG[istio-agent
key+CSR] -->|SA token| CA[istiod CA] CA -->|signed SVID| AG end subgraph Verify["검증 (mTLS handshake)"] AG -->|present SVID| HS[peer chain-verify
vs trust bundle] HS --> PID[peer SPIFFE ID
from SAN URI] end subgraph Authz["인가 (RBAC filter)"] PID -->|scheme 제거| PR[source.principal] PR --> DEC{ALLOW / DENY} end ``` 이 파이프라인을 따라가며 채워 넣는다 — **신원이 무엇이고**(§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다. ```text spiffe:///ns//sa/ 예: 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/` | Pod의 namespace | 격리 단위 | | `sa/` | Pod의 ServiceAccount | **신원의 실질 주체** | 핵심은 Istio 신원이 **Pod·IP가 아니라 Kubernetes ServiceAccount에 1:1로 묶인다**는 점이다. 같은 SA를 쓰는 Pod가 10개로 스케일아웃해도 신원은 하나다. 따라서 "신원을 분리하려면 ServiceAccount를 분리한다"가 운영 제1원칙이 된다 — Deployment마다 전용 SA를 주는 습관이 곧 최소권한 정책의 토대다. > [!key] authz 표기와의 차이 > 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는 비워두거나 무시). ```text 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 파이프라인의 "발급" 박스를 펼친 것이다. ```mermaid flowchart TD SA[Pod ServiceAccount
projected SA token] --> AG[istio-agent
인 Pod] AG -->|CSR + SA token| CA[istiod CA
citadel] CA -->|토큰 검증: TokenReview
→ 신원 확정 → 서명| AG AG -->|SDS push: cert chain + key + root| EN[Envoy
SDS client] EN -.메모리상 보관, 디스크 미기록.-> EN ``` 단계별 메커니즘(이 4단계가 신뢰 사슬의 전부다): 1. **private key는 Pod 안에서 생성.** istio-agent가 key를 만들고, 그에 대한 CSR(certificate signing request)을 만든다. private key는 절대 Pod 밖으로 나가지 않는다 — 그래서 네트워크·CA를 털어도 key는 못 얻는다. 2. **신원 증명은 SA token으로.** agent는 Kubernetes가 마운트한 **projected ServiceAccount token**(audience `istio-ca`)을 CSR과 함께 istiod에 보낸다. "내가 누구다"라는 *주장*의 근거다. 3. **istiod CA가 토큰을 검증하고 서명.** istiod는 그 SA token을 Kubernetes API의 `TokenReview`로 검증해 "이 요청자가 정말 그 namespace/SA의 워크로드인가"를 확인한 뒤, 거기서 도출한 SPIFFE ID를 SAN에 박아 cert를 서명·발급한다. **신원을 요청자 말이 아니라 K8s API가 보증하는 것**이 핵심 — 주장을 K8s가 사실로 승격시킨다. 4. **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를 교체한다. ```text 기본 수명 24h 재발급 시점 수명의 약 50% 경과 시 (절반에서 미리 갱신) 교체 방식 SDS push → Envoy hot reload (재시작·재연결 불필요) 관측 포인트 istioctl proxy-config secret 의 "Valid Cert" 의 issue/expire 시각이 주기적으로 갱신됨 ``` 단명 cert의 의의: 키가 유출돼도 **노출 창이 수 시간으로 제한**된다. 또 디스크에 key가 없으니 Pod 파일시스템 탈취만으로는 신원을 훔칠 수 없다. SDS·로테이션의 data-plane 동기화 측면 detail은 [data-plane sync 상태](/public/istio/xds__note-data-plane-sync-state.html)를, secret을 Envoy admin에서 직접 들여다보는 진단은 [Envoy admin API 진단](/public/istio/xds__note-envoy-admin-api-diagnosis.html)을 본다. ## 05. SVID를 trust bundle로 검증하는 절차 (두 층위) 발급된 SVID가 mTLS handshake에 올라오면, 받은 쪽은 그것을 신뢰할지 결정해야 한다 — §01 파이프라인의 "검증" 박스다. 신뢰의 근거는 cert가 **공유된 root CA(trust bundle)로 서명되었는지**다. trust bundle은 istio-agent가 SDS로 함께 받아(§04 4단계의 `root`) Envoy의 validation context에 채워둔다. 검증은 **별개의 두 층위**이고, 이 둘을 구분 못 하면 "왜 연결은 되는데 정책이 안 먹나"를 못 푼다. ```text [chain 검증] 받은 leaf SVID → (intermediate) → root CA 까지 서명 chain이 trust bundle의 root로 연결되고 유효기간·서명이 맞는가. → 표준 X.509 PKI 검증. 실패하면 handshake 자체가 깨짐. [identity 검증] chain이 유효해도 "그래서 누구냐"는 SAN URI를 꺼내야 안다. 이 값이 secure naming(기대 SA)·authz principal과 대조된다. ``` ```mermaid sequenceDiagram participant C as Client sidecar participant S as Server sidecar Note over C,S: TLS 1.3 mutual handshake (1 round-trip) C->>S: ClientHello S->>C: ServerHello + Server SVID + CertificateRequest C->>S: Client SVID Note over C,S: 양측이 상대 SVID를 trust bundle(root CA)로 chain 검증 Note over C,S: 검증 OK면 상대 SAN URI = peer SPIFFE ID 확보 S-->>C: Finished Note over S: 이후 RBAC filter가 peer principal로 ALLOW/DENY 판정 ``` 여기서 자주 헷갈리는 점: **handshake 성공 = 인가 통과가 아니다.** handshake(=chain 검증)는 "상대가 trust-domain 안의 검증된 워크로드임"까지만 보장한다. "그 워크로드가 이 작업을 해도 되는가"는 그 다음 단계인 AuthorizationPolicy(RBAC filter)가 identity 검증으로 뽑은 SAN principal로 결정한다([인가 평가 순서 CUSTOM→DENY→ALLOW](/public/istio/sec__src-authorizationpolicy-mental-model.html)). ## 06. secure naming — client 쪽 신원 검증 §05의 identity 검증은 서버만 하는 게 아니다. **client Envoy도 자신이 붙은 서버의 SVID를 검증**한다 — 단순 chain 검증을 넘어, "내가 호출하려던 service에 **기대되는 ServiceAccount**가 서버 cert의 SAN과 일치하는가"를 본다. 이를 secure naming이라 한다. ```text 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와 무엇이 다른가 ```text 일반 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](/public/istio/sec__src-authorizationpolicy-mental-model.html)에 표로 정리돼 있다. > [!key] PeerAuthentication과의 분업 > mTLS를 **강제**(평문 거부)하는 것은 이 신원 메커니즘이 아니라 PeerAuthentication STRICT의 몫이다. PERMISSIVE면 평문도 받아들여 peer cert가 없는 채로 들어오고, 그러면 principal 조건이 그냥 매칭 실패할 뿐 "평문이라서 거부"가 명시되는 게 아니다. 신원·인증·인가의 역할 분담은 [보안 리소스 trio](/public/istio/sec__note-security-resource-trio.html)를 본다. ## 08. 예시 — 신원을 손으로 확인하고 검증하기 추상적 주장을 실제 cluster에서 떨어뜨려 확인하는 절차다. 모든 출력은 §01 파이프라인의 어느 박스를 보는 것인지 함께 표시한다. **(1) 발급된 secret이 Envoy 메모리에 있는가** — 발급 박스의 결과물 확인. ```bash istioctl proxy-config secret . # 기대 출력 (요약) # 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). ```bash istioctl proxy-config secret . -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:///ns//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: ```text 신원 확인 istioctl proxy-config secret → 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 성능 요인](/public/istio/arch__note-control-plane-performance-factors.html)과 함께 본다. - **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)가 함께 있어야 "검증된 신원 중 허용된 것만" 통과한다. **관련 검증** → [Egress mTLS 리포트 — HTTPS over mTLS (ISTIO_MUTUAL)](/public/istio/gw__report-2026-06-08_egress-mtls.html)