Skip to content

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

|

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

들어가며

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의 내부 구조를 살펴보겠습니다.

Istio Security Model Analysis: mTLS, Authentication, Authorization

Introduction

Istio's security model implements "zero trust networking" at the service mesh level. All service-to-service communication is encrypted by default, and every request goes through authentication and authorization.

This post analyzes the internal implementation of Istio security's three core pillars: identity, authentication, and authorization.

Certificate Lifecycle

istiod CA Role

istiod acts as a built-in CA (Certificate Authority) managing certificates for all workloads in the mesh:

istiod CA
├── Root Certificate (self-signed or issued by external CA)
├── Intermediate CA Certificate (optional)
└── Workload Certificates (issued to each workload)
    ├── frontend → spiffe://cluster.local/ns/prod/sa/frontend
    ├── reviews → spiffe://cluster.local/ns/prod/sa/reviews
    └── ratings → spiffe://cluster.local/ns/prod/sa/ratings

Detailed Certificate Issuance Flow

[1] Pod starts → istio-agent initializes
[2] istio-agent generates RSA 2048 or ECDSA P-256 key pair
[3] Creates CSR including SPIFFE ID
    (spiffe://cluster.local/ns/NAMESPACE/sa/SA_NAME)
[4] Sends CSR to istiod along with Kubernetes ServiceAccount token
[5] istiod validation:
    ├── ServiceAccount token validity (TokenReview API)
    ├── Token namespace/SA matches CSR SPIFFE ID
    └── CSR format validity
[6] istiod CA signs X.509 certificate
    ├── Subject: SPIFFE ID
    ├── SAN (Subject Alternative Name): SPIFFE URI
    ├── Validity: 24 hours (default)
    └── Key Usage: Digital Signature, Key Encipherment
[7] Returns signed certificate + CA chain to istio-agent
[8] istio-agent delivers certificate to Envoy via SDS
[9] Automatic renewal before expiry (around 50% of validity period)

External CA Integration

Production environments can use external CAs:

External CA integration methods:
├── 1. Plug-in CA: Mount external CA intermediate cert in istiod
│   └── istiod signs workload certs as intermediate CA
├── 2. CSR API: Use Kubernetes CertificateSigningRequest API
│   └── External signer approves/signs Kubernetes CSRs
└── 3. Custom CA (istio-csr): cert-manager + Istio CSR Agent
    └── cert-manager issues certs from external CA (Vault, AWS ACM, etc.)

mTLS Handshake Flow

Sidecar-to-Sidecar mTLS

The mTLS connection establishment between two services:

Client Pod (frontend)              Server Pod (reviews)
[App][Envoy Proxy]     ←→     [Envoy Proxy][App]

1. App sends request to reviews:9080
   (plaintext HTTP, internal to localhost)
2. iptables redirects traffic to Envoy (15001)
3. Envoy checks TLS settings for destination cluster
   (DestinationRule or auto mTLS)
4. TLS handshake begins:
   ├── ClientHello (supported TLS versions, cipher suites)
   ├── ServerHello + Server Certificate
   │   └── reviews presents its SPIFFE certificate
   ├── Client Certificate
   │   └── frontend presents its SPIFFE certificate
   ├── Certificate Verification (bidirectional)
   │   ├── CA chain validation
   │   ├── SPIFFE ID verification
   │   └── Certificate expiry check
   └── Finished (session key exchange complete)
5. HTTP request sent over encrypted channel
6. Server Envoy terminates TLS
7. Forwards as plaintext to App (localhost)

Auto mTLS

Istio enables auto mTLS by default:

Does the destination have a sidecar?
    ├── Yes → automatically use mTLS
       (even without TLS settings in DestinationRule)
    └── No → use plaintext
        (forcing mTLS to sidecar-less services fails)

This maintains communication even when sidecars are being gradually injected.

PeerAuthentication Internals

Policy Scope and Priority

Priority (highest first):
1. Workload-level (selector specified)
2. Namespace-level (no selector, specific namespace)
3. Mesh-level (istio-system namespace, no selector)

Envoy Configuration by mTLS Mode

STRICT mode:

{
  "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 certificate"
            }
          }
        }
      }
    }
  ]
}

Connections without a valid client certificate are immediately rejected.

PERMISSIVE mode:

{
  "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"
      }
    }
  ]
}

Two filter chains exist: one for TLS connections and one for plaintext. The TLS Inspector classifies connections.

Per-Port mTLS Configuration

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 # Disable mTLS for health check port

RequestAuthentication Internals

JWT Validation Flow

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'

Processing in Envoy:

Request arrives (Authorization: Bearer TOKEN)
JWT Authn Filter (envoy.filters.http.jwt_authn)
    ├── Extract JWT token (Authorization header)
    ├── Check JWKS cache
    │   ├── Cache hit → verify signature with public key
    │   └── Cache miss → download keys from jwksUri
    ├── Token validation:
    │   ├── Signature validity
    │   ├── issuer (iss) match
    │   ├── Expiry (exp) check
    │   └── audience (aud) check (if configured)
    ├── Validation success:
    │   ├── Store payload in filter metadata
    │   ├── forwardOriginalToken: true → preserve original token
    │   └── outputPayloadToHeader → add payload to specified header
    └── Validation failure:
        ├── Invalid JWT401 Unauthorized
        └── No JWT → request passes through (unauthenticated)

Handling Requests Without JWT

Important characteristics of RequestAuthentication:

  • If JWT is present, it must be valid (invalid returns 401)
  • If JWT is absent, the request passes through (unauthenticated)
  • To also reject requests without JWT, use with AuthorizationPolicy
# Pattern to also reject requests without JWT
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: require-jwt
spec:
  selector:
    matchLabels:
      app: reviews
  action: DENY
  rules:
    - from:
        - source:
            notRequestPrincipals: ['*']

AuthorizationPolicy Internals

Evaluation Order

Request arrives
[1] CUSTOM policy evaluation
    ├── Matched and denied → 403 Forbidden
    ├── Matched and allowed → proceed to [2]
    └── Not matched → proceed to [2]
[2] DENY policy evaluation
    ├── Matched403 Forbidden
    └── Not matched → proceed to [3]
[3] Check for ALLOW policies
    ├── No ALLOW policies → allow (default allow)
    └── ALLOW policies exist:
        ├── Matched → allow
        └── Not matched → 403 Forbidden

Translation to 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 filter configuration:

{
  "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 Field Mapping

AuthorizationPolicyEnvoy RBACDescription
source.principalsauthenticated.principal_nameSPIFFE ID matching
source.namespacesauthenticated.principal_name (prefix)Namespace matching
source.ipBlockssource_ipIP range matching
source.requestPrincipalsmetadata (JWT claims)JWT subject matching

Operation Field Mapping

AuthorizationPolicyEnvoy RBACDescription
operation.hostsheader (:authority)Host matching
operation.methodsheader (:method)HTTP method matching
operation.pathsurl_pathPath matching
operation.portsdestination_portPort matching

Trust Domain and Migration

Trust Domain Overview

Trust Domain: scope of trust for a CA that issues certificates

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

When changing CA or migrating trust domains:

# Configure trust domain aliases in MeshConfig
meshConfig:
  trustDomain: 'new-domain.example.com'
  trustDomainAliases:
    - 'old-domain.example.com'

This allows certificates issued under the old trust domain to continue being trusted.

External Authorization Integration (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/*']

Register External Authz Provider in MeshConfig

meshConfig:
  extensionProviders:
    - name: 'opa-ext-authz'
      envoyExtAuthzGrpc:
        service: 'opa.opa-system.svc.cluster.local'
        port: 9191
        timeout: 5s
        failOpen: false

Request Flow

Request arrives
Envoy ext_authz filter
    ├── Send authorization request to OPA service via gRPC
    │   ├── Request headers
    │   ├── Path, method
    │   ├── Source principal (SPIFFE ID)
    │   └── Custom attributes
    ├── OPA response:
    │   ├── ALLOWcontinue processing request
    │   ├── DENYreturn 403
    │   └── Timeout:
    │       ├── failOpen: true → allow request
    │       └── failOpen: false → deny request
    └── OPA can include additional headers in response
        (e.g., authorization context info)

Security Best Practices

1. Gradual mTLS Rollout

# Phase 1: Mesh-wide PERMISSIVE
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: istio-system
spec:
  mtls:
    mode: PERMISSIVE

# Phase 2: Per-namespace STRICT transition
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: production
spec:
  mtls:
    mode: STRICT

# Phase 3: Mesh-wide STRICT
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: istio-system
spec:
  mtls:
    mode: STRICT

2. Default Deny Policy

# Deny all requests by default, explicitly allow
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: deny-all
  namespace: production
spec: {} # Empty spec = ALLOW action, empty rules = deny all requests

3. Namespace Isolation

# Allow only traffic within the same namespace
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: allow-same-namespace
  namespace: production
spec:
  action: ALLOW
  rules:
    - from:
        - source:
            namespaces: ['production']

Debugging Tools

# Check mTLS status
istioctl authn tls-check PODNAME.NAMESPACE

# AuthorizationPolicy application status
istioctl x authz check PODNAME.NAMESPACE

# Check certificate information
istioctl proxy-config secret PODNAME.NAMESPACE -o json

# Enable Envoy RBAC debug logging
kubectl exec PODNAME -c istio-proxy -- \
  curl -X POST "localhost:15000/logging?rbac=debug"

# Check RBAC denial statistics
kubectl exec PODNAME -c istio-proxy -- \
  curl -s localhost:15000/stats | grep rbac

Conclusion

Istio's security model consists of three layers:

  1. Identity: SPIFFE-based workload identity and automatic certificate management
  2. Authentication: mTLS (PeerAuthentication) and JWT (RequestAuthentication)
  3. Authorization: RBAC-based fine-grained access control (AuthorizationPolicy)

These three layers work organically within Envoy proxy's filter chain to implement zero trust security.

In the next post, we will explore the internal architecture of Istio Ambient Mesh.