- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 들어가며
- 인증서 라이프사이클
- mTLS 핸드셰이크 흐름
- PeerAuthentication 내부 동작
- RequestAuthentication 내부 동작
- AuthorizationPolicy 내부 동작
- Trust Domain과 마이그레이션
- 외부 인가 서비스 통합 (OPA)
- 보안 베스트 프랙티스
- 디버깅 도구
- 마무리
들어가며
Istio의 보안 모델은 "제로 트러스트 네트워킹"을 서비스 메시 수준에서 구현합니다. 모든 서비스 간 통신이 기본적으로 암호화되고, 모든 요청이 인증 및 인가를 거치는 아키텍처입니다.
이 글에서는 Istio 보안의 세 가지 핵심 축인 아이덴티티, 인증(Authentication), **인가(Authorization)**의 내부 구현을 분석합니다.
인증서 라이프사이클
istiod CA의 역할
istiod는 내장 CA(Certificate Authority)로서 메시 내 모든 워크로드의 인증서를 관리합니다:
istiod CA
├── Root Certificate (자체 서명 또는 외부 CA에서 발급)
├── Intermediate CA Certificate (선택사항)
└── Workload Certificates (각 워크로드에 발급)
├── frontend → spiffe://cluster.local/ns/prod/sa/frontend
├── reviews → spiffe://cluster.local/ns/prod/sa/reviews
└── ratings → spiffe://cluster.local/ns/prod/sa/ratings
인증서 발급 상세 흐름
[1] Pod 시작 → istio-agent 초기화
│
[2] istio-agent가 RSA 2048 또는 ECDSA P-256 키 쌍 생성
│
[3] SPIFFE ID를 포함한 CSR 생성
(spiffe://cluster.local/ns/NAMESPACE/sa/SA_NAME)
│
[4] Kubernetes ServiceAccount 토큰과 함께 CSR을 istiod에 전송
│
[5] istiod 검증:
├── ServiceAccount 토큰 유효성 (TokenReview API)
├── 토큰의 네임스페이스/SA가 CSR의 SPIFFE ID와 일치하는지
└── CSR 형식 유효성
│
[6] istiod CA가 X.509 인증서 서명
├── Subject: SPIFFE ID
├── SAN (Subject Alternative Name): SPIFFE URI
├── 유효 기간: 24시간 (기본값)
└── Key Usage: Digital Signature, Key Encipherment
│
[7] 서명된 인증서 + CA 체인을 istio-agent에 반환
│
[8] istio-agent가 SDS를 통해 Envoy에 인증서 전달
│
[9] 만료 전 자동 갱신 (유효 기간의 약 50% 시점)
외부 CA 통합
프로덕션 환경에서는 외부 CA를 사용할 수 있습니다:
외부 CA 연동 방식:
├── 1. Plug-in CA: istiod에 외부 CA의 중간 인증서를 마운트
│ └── istiod가 중간 CA로서 워크로드 인증서 서명
│
├── 2. CSR API: Kubernetes CertificateSigningRequest API 사용
│ └── 외부 서명자가 Kubernetes CSR을 승인/서명
│
└── 3. Custom CA (istio-csr): cert-manager + Istio CSR Agent
└── cert-manager가 외부 CA(Vault, AWS ACM 등)에서 인증서 발급
mTLS 핸드셰이크 흐름
사이드카 간 mTLS
두 서비스 간의 mTLS 연결 수립 과정:
Client Pod (frontend) Server Pod (reviews)
[App] → [Envoy Proxy] ←→ [Envoy Proxy] → [App]
1. App이 reviews:9080에 요청
(평문 HTTP, localhost 내부)
│
2. iptables가 트래픽을 Envoy(15001)로 리다이렉트
│
3. Envoy가 대상 클러스터의 TLS 설정 확인
(DestinationRule 또는 auto mTLS)
│
4. TLS 핸드셰이크 시작:
├── ClientHello (지원하는 TLS 버전, 암호화 스위트)
├── ServerHello + Server Certificate
│ └── reviews의 SPIFFE 인증서 제시
├── Client Certificate
│ └── frontend의 SPIFFE 인증서 제시
├── Certificate Verification (양방향)
│ ├── CA 체인 검증
│ ├── SPIFFE ID 검증
│ └── 인증서 만료 확인
└── Finished (세션 키 교환 완료)
│
5. 암호화된 채널에서 HTTP 요청 전송
│
6. Server Envoy가 TLS 종료
│
7. 평문으로 App에 전달 (localhost)
Auto mTLS
Istio는 기본적으로 auto mTLS를 활성화합니다:
대상에 사이드카가 있나?
│
├── Yes → 자동으로 mTLS 사용
│ (DestinationRule에 TLS 설정 없어도)
│
└── No → 평문 사용
(사이드카 없는 서비스에 mTLS 강제 시 실패)
이를 통해 사이드카가 점진적으로 주입되는 환경에서도 통신이 유지됩니다.
PeerAuthentication 내부 동작
정책 범위와 우선순위
우선순위 (높은 순):
1. Workload-level (selector 지정)
2. Namespace-level (selector 미지정, 특정 네임스페이스)
3. Mesh-level (istio-system 네임스페이스, selector 미지정)
mTLS 모드별 Envoy 구성
STRICT 모드:
{
"filter_chains": [
{
"filter_chain_match": {},
"transport_socket": {
"name": "envoy.transport_sockets.tls",
"typed_config": {
"require_client_certificate": true,
"common_tls_context": {
"validation_context": {
"trusted_ca": "CA 인증서"
}
}
}
}
}
]
}
클라이언트 인증서가 없거나 유효하지 않으면 연결이 즉시 거부됩니다.
PERMISSIVE 모드:
{
"filter_chains": [
{
"filter_chain_match": {
"transport_protocol": "tls"
},
"transport_socket": {
"name": "envoy.transport_sockets.tls",
"typed_config": {
"require_client_certificate": true
}
}
},
{
"filter_chain_match": {},
"transport_socket": {
"name": "envoy.transport_sockets.raw_buffer"
}
}
]
}
두 개의 필터 체인이 존재합니다: TLS 연결용과 평문 연결용. TLS Inspector가 연결을 분류합니다.
포트별 mTLS 설정
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: reviews-mtls
namespace: production
spec:
selector:
matchLabels:
app: reviews
mtls:
mode: STRICT
portLevelMtls:
8080:
mode: STRICT
15021:
mode: DISABLE # 헬스 체크 포트는 mTLS 비활성화
RequestAuthentication 내부 동작
JWT 검증 흐름
apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
name: jwt-auth
spec:
selector:
matchLabels:
app: reviews
jwtRules:
- issuer: 'https://auth.example.com'
jwksUri: 'https://auth.example.com/.well-known/jwks.json'
forwardOriginalToken: true
outputPayloadToHeader: 'x-jwt-payload'
Envoy에서의 처리:
요청 도착 (Authorization: Bearer TOKEN)
│
▼
JWT Authn Filter (envoy.filters.http.jwt_authn)
│
├── JWT 토큰 추출 (Authorization 헤더)
│
├── JWKS 캐시 확인
│ ├── 캐시 히트 → 공개 키로 서명 검증
│ └── 캐시 미스 → jwksUri에서 키 다운로드
│
├── 토큰 검증:
│ ├── 서명 유효성
│ ├── issuer(iss) 일치 여부
│ ├── 만료(exp) 확인
│ └── audience(aud) 확인 (설정된 경우)
│
├── 검증 성공:
│ ├── 페이로드를 필터 메타데이터에 저장
│ ├── forwardOriginalToken: true → 원본 토큰 유지
│ └── outputPayloadToHeader → 페이로드를 지정 헤더에 추가
│
└── 검증 실패:
├── 유효하지 않은 JWT → 401 Unauthorized
└── JWT 없음 → 요청 통과 (인증되지 않은 상태로)
JWT가 없는 요청의 처리
RequestAuthentication의 중요한 특성:
- JWT가 있으면 반드시 유효해야 함 (유효하지 않으면 401)
- JWT가 없으면 요청이 통과함 (인증되지 않은 상태)
- JWT 없는 요청도 거부하려면 AuthorizationPolicy와 함께 사용
# JWT가 없는 요청도 거부하는 패턴
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: require-jwt
spec:
selector:
matchLabels:
app: reviews
action: DENY
rules:
- from:
- source:
notRequestPrincipals: ['*']
AuthorizationPolicy 내부 동작
평가 순서
요청 도착
│
▼
[1] CUSTOM 정책 평가
├── 매칭되고 거부 → 403 Forbidden
├── 매칭되고 허용 → [2]로 진행
└── 매칭 안 됨 → [2]로 진행
│
▼
[2] DENY 정책 평가
├── 매칭됨 → 403 Forbidden
└── 매칭 안 됨 → [3]로 진행
│
▼
[3] ALLOW 정책 존재 여부 확인
├── ALLOW 정책 없음 → 허용 (기본 허용)
└── ALLOW 정책 있음:
├── 매칭됨 → 허용
└── 매칭 안 됨 → 403 Forbidden
Envoy RBAC Filter로의 변환
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: allow-frontend
namespace: production
spec:
selector:
matchLabels:
app: reviews
action: ALLOW
rules:
- from:
- source:
principals: ['cluster.local/ns/production/sa/frontend']
to:
- operation:
methods: ['GET']
paths: ['/api/*']
Envoy RBAC 필터 구성:
{
"name": "envoy.filters.http.rbac",
"typed_config": {
"rules": {
"action": "ALLOW",
"policies": {
"allow-frontend": {
"permissions": [
{
"and_rules": {
"rules": [
{
"header": {
"name": ":method",
"string_match": {
"exact": "GET"
}
}
},
{
"url_path": {
"path": {
"prefix": "/api/"
}
}
}
]
}
}
],
"principals": [
{
"authenticated": {
"principal_name": {
"exact": "spiffe://cluster.local/ns/production/sa/frontend"
}
}
}
]
}
}
}
}
}
Source 필드 매핑
| AuthorizationPolicy | Envoy RBAC | 설명 |
|---|---|---|
| source.principals | authenticated.principal_name | SPIFFE ID 매칭 |
| source.namespaces | authenticated.principal_name (prefix) | 네임스페이스 매칭 |
| source.ipBlocks | source_ip | IP 범위 매칭 |
| source.requestPrincipals | metadata (JWT claims) | JWT 주체 매칭 |
Operation 필드 매핑
| AuthorizationPolicy | Envoy RBAC | 설명 |
|---|---|---|
| operation.hosts | header (:authority) | 호스트 매칭 |
| operation.methods | header (:method) | HTTP 메서드 매칭 |
| operation.paths | url_path | 경로 매칭 |
| operation.ports | destination_port | 포트 매칭 |
Trust Domain과 마이그레이션
Trust Domain 개요
Trust Domain: 인증서를 발급하는 CA의 신뢰 범위
cluster-1: trust domain = "cluster-1.example.com"
└── spiffe://cluster-1.example.com/ns/prod/sa/frontend
cluster-2: trust domain = "cluster-2.example.com"
└── spiffe://cluster-2.example.com/ns/prod/sa/frontend
Trust Domain Migration
CA를 변경하거나 trust domain을 마이그레이션할 때:
# MeshConfig에서 trust domain alias 설정
meshConfig:
trustDomain: 'new-domain.example.com'
trustDomainAliases:
- 'old-domain.example.com'
이를 통해 이전 trust domain으로 발급된 인증서도 계속 신뢰할 수 있습니다.
외부 인가 서비스 통합 (OPA)
CUSTOM AuthorizationPolicy
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: ext-authz
namespace: production
spec:
selector:
matchLabels:
app: reviews
action: CUSTOM
provider:
name: 'opa-ext-authz'
rules:
- to:
- operation:
paths: ['/api/*']
MeshConfig에 외부 인가 제공자 등록
meshConfig:
extensionProviders:
- name: 'opa-ext-authz'
envoyExtAuthzGrpc:
service: 'opa.opa-system.svc.cluster.local'
port: 9191
timeout: 5s
failOpen: false
요청 흐름
요청 도착
│
▼
Envoy ext_authz 필터
│
├── gRPC로 OPA 서비스에 인가 요청 전송
│ ├── 요청 헤더
│ ├── 경로, 메서드
│ ├── 소스 principal (SPIFFE ID)
│ └── 커스텀 속성
│
├── OPA 응답:
│ ├── ALLOW → 요청 계속 처리
│ ├── DENY → 403 반환
│ └── 타임아웃:
│ ├── failOpen: true → 요청 허용
│ └── failOpen: false → 요청 거부
│
└── OPA에서 추가 헤더를 응답에 포함할 수 있음
(예: 인가 컨텍스트 정보)
보안 베스트 프랙티스
1. 점진적 mTLS 적용
# 1단계: 메시 전체 PERMISSIVE
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: istio-system
spec:
mtls:
mode: PERMISSIVE
# 2단계: 네임스페이스별 STRICT 전환
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: production
spec:
mtls:
mode: STRICT
# 3단계: 메시 전체 STRICT
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: istio-system
spec:
mtls:
mode: STRICT
2. 기본 거부 정책
# 모든 요청을 기본 거부하고, 명시적으로 허용
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: deny-all
namespace: production
spec: {} # 빈 spec = ALLOW 액션, 빈 rules = 모든 요청 거부
3. 네임스페이스 격리
# 같은 네임스페이스 내부 트래픽만 허용
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: allow-same-namespace
namespace: production
spec:
action: ALLOW
rules:
- from:
- source:
namespaces: ['production']
디버깅 도구
# mTLS 상태 확인
istioctl authn tls-check PODNAME.NAMESPACE
# AuthorizationPolicy 적용 상태
istioctl x authz check PODNAME.NAMESPACE
# 인증서 정보 확인
istioctl proxy-config secret PODNAME.NAMESPACE -o json
# Envoy RBAC 디버그 로그 활성화
kubectl exec PODNAME -c istio-proxy -- \
curl -X POST "localhost:15000/logging?rbac=debug"
# RBAC 거부 통계 확인
kubectl exec PODNAME -c istio-proxy -- \
curl -s localhost:15000/stats | grep rbac
마무리
Istio의 보안 모델은 세 가지 계층으로 구성됩니다:
- 아이덴티티: SPIFFE 기반 워크로드 아이덴티티와 자동 인증서 관리
- 인증: mTLS(PeerAuthentication)와 JWT(RequestAuthentication)
- 인가: RBAC 기반 세밀한 접근 제어(AuthorizationPolicy)
이 세 계층이 Envoy 프록시의 필터 체인에서 유기적으로 동작하여 제로 트러스트 보안을 구현합니다.
다음 글에서는 Istio Ambient Mesh의 내부 구조를 살펴보겠습니다.