Skip to content
Published on

Istio 보안 모델 분석: mTLS, 인증, 인가

Authors

들어가며

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)
    ├── 토큰의 네임스페이스/SACSRSPIFFE ID와 일치하는지
    └── CSR 형식 유효성
[6] istiod CAX.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 → 페이로드를 지정 헤더에 추가
    └── 검증 실패:
        ├── 유효하지 않은 JWT401 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 필드 매핑

AuthorizationPolicyEnvoy RBAC설명
source.principalsauthenticated.principal_nameSPIFFE ID 매칭
source.namespacesauthenticated.principal_name (prefix)네임스페이스 매칭
source.ipBlockssource_ipIP 범위 매칭
source.requestPrincipalsmetadata (JWT claims)JWT 주체 매칭

Operation 필드 매핑

AuthorizationPolicyEnvoy RBAC설명
operation.hostsheader (:authority)호스트 매칭
operation.methodsheader (:method)HTTP 메서드 매칭
operation.pathsurl_path경로 매칭
operation.portsdestination_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 → 요청 계속 처리
    │   ├── DENY403 반환
    │   └── 타임아웃:
    │       ├── 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의 보안 모델은 세 가지 계층으로 구성됩니다:

  1. 아이덴티티: SPIFFE 기반 워크로드 아이덴티티와 자동 인증서 관리
  2. 인증: mTLS(PeerAuthentication)와 JWT(RequestAuthentication)
  3. 인가: RBAC 기반 세밀한 접근 제어(AuthorizationPolicy)

이 세 계층이 Envoy 프록시의 필터 체인에서 유기적으로 동작하여 제로 트러스트 보안을 구현합니다.

다음 글에서는 Istio Ambient Mesh의 내부 구조를 살펴보겠습니다.