- Authors

- Name
- Youngju Kim
- @fjvbn20031
- Introduction
- Certificate Lifecycle
- mTLS Handshake Flow
- PeerAuthentication Internals
- RequestAuthentication Internals
- AuthorizationPolicy Internals
- Trust Domain and Migration
- External Authorization Integration (OPA)
- Security Best Practices
- Debugging Tools
- Conclusion
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 JWT → 401 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
├── Matched → 403 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
| AuthorizationPolicy | Envoy RBAC | Description |
|---|---|---|
| source.principals | authenticated.principal_name | SPIFFE ID matching |
| source.namespaces | authenticated.principal_name (prefix) | Namespace matching |
| source.ipBlocks | source_ip | IP range matching |
| source.requestPrincipals | metadata (JWT claims) | JWT subject matching |
Operation Field Mapping
| AuthorizationPolicy | Envoy RBAC | Description |
|---|---|---|
| operation.hosts | header (:authority) | Host matching |
| operation.methods | header (:method) | HTTP method matching |
| operation.paths | url_path | Path matching |
| operation.ports | destination_port | Port 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:
│ ├── ALLOW → continue processing request
│ ├── DENY → return 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:
- Identity: SPIFFE-based workload identity and automatic certificate management
- Authentication: mTLS (PeerAuthentication) and JWT (RequestAuthentication)
- 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.