Overview
When Istio injects sidecars into pods, all service-to-service traffic passes through Envoy proxies. Istio adds its own mTLS layer (using SPIFFE certificates) between sidecars. This raises a practical question: what happens when the application itself also has TLS configured? This note covers how TLS works across each hop, what happens in various combinations of app TLS + mesh mTLS, how kube-apiserver webhook calls interact with Istio, and how egress HTTPS to the internet behaves.
For Istioβs mTLS mechanics, SPIFFE identity, and PeerAuthentication policy, see Istio Security.
For the iptables-based traffic interception that makes all of this transparent, see Istio Architecture Deep Dive.
The Three Legs of Traffic
Every request between two meshed pods traverses three distinct network segments, each with different encryption characteristics:
Pod A (Client) Pod B (Server)
ββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββ
β β β β
β ββββββββββββ iptables β β iptables βββββββββββββ β
β βClient AppββββredirectββββΆβ ββββββββββ Network ββββββββββ βββββredirectββββServer Appβ β
β β β β β Envoy ββββββββββββ Envoy β β β β β
β β βββββββββββββββββ βsidecar βIstio mTLSβsidecar β ββββββββββββββββΆβ β β
β ββββββββββββ localhost β ββββββββββ (SPIFFE) ββββββββββ β localhost ββββββββββββ β
ββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββ
Leg 1 Leg 2 Leg 3
localhost, PLAINTEXT network, Istio mTLS localhost, PLAINTEXT
(app β own sidecar) (sidecar β sidecar) (sidecar β app)
Leg 1: Client App to Its Own Sidecar
Always plaintext. The app calls connect("server-svc:8080"), but iptables silently redirects to 127.0.0.1:15001 (Envoyβs outbound listener). The app never opens a real TCP connection to the remote server β the connection stays on loopback within the podβs network namespace.
Envoy recovers the original destination via getsockopt(SO_ORIGINAL_DST) from the kernelβs conntrack table, then routes based on that.
Leg 2: Client Sidecar to Server Sidecar
Istio mTLS. When PeerAuthentication is STRICT or PERMISSIVE (the default), both sidecars perform a mutual TLS handshake using SPIFFE X.509 SVIDs issued by istiod. The ALPN is istio-peer-exchange. Both sides verify each otherβs certificates against the mesh root CA. Typically TLS 1.3 with ECDHE + AES-256-GCM.
This leg is the only one where data traverses the physical/virtual network. Istio mTLS ensures encryption and identity authentication on this segment.
Leg 3: Server Sidecar to Server App
Plaintext by default. The server-side Envoy terminates the Istio mTLS, extracts the decrypted request, and forwards it to 127.0.0.1:<app-port>. This is again loopback β no data on the wire.
What Happens When the App Also Has TLS
Scenario 1: No Special Configuration β Connection Fails
If the server app listens with TLS (e.g., on :8443) and you donβt tell Istio, the server-side sidecar sends plaintext to the app (its default behavior). The app expects a TLS ClientHello but receives raw HTTP β handshake failure.
Client App ββplainβββΆ Client Envoy ββIstio mTLSβββΆ Server Envoy ββplaintextβββΆ Server App (:8443 TLS)
β
App expects TLS ClientHello
Gets "GET /..." in plaintext
β connection error β
Scenario 2: Sidecar Configured to TLS Into the App β Double Encryption
You can configure the sidecarβs upstream connection to use TLS when talking to the app. This results in two independent TLS layers:
On the wire between pods:
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Istio mTLS (sidecar β sidecar) β
β ββββββββββββββββββββββββββββββββββββββββββββββ β
β β App TLS (sidecar β app on localhost) β β
β β ββββββββββββββββββββββββββββββββββββββββ β β
β β β GET /api/data HTTP/1.1 β β β
β β ββββββββββββββββββββββββββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
Performance cost:
- 2x TLS handshake latency per new connection (each involves ECDHE key exchange + cert verification)
- 2x symmetric encryption overhead (AES-GCM on every byte, twice)
- 2x per-connection TLS state memory
- 2x operational complexity (two sets of certificates, two rotation schedules, two layers to debug)
Scenario 3: PASSTHROUGH Mode β App Handles TLS End-to-End
Istio passes the raw bytes through without terminating or originating TLS:
Client App ββapp TLSβββΆ Client Envoy ββTCP passthroughβββΆ Server Envoy ββTCP passthroughβββΆ Server App
(routes by SNI only, (forwards raw bytes,
opaque TCP proxy) no Istio mTLS)
No Istio mTLS at all. Sidecars are reduced to TCP proxies. You lose:
- L7 metrics (request count, latency by path, status codes)
- Header-based routing, retries, fault injection
AuthorizationPolicyrules on HTTP paths/methods- Distributed tracing header propagation
Only TCP-level metrics (bytes in/out, connection count) and source-identity-based policies survive.
Recommended Architecture for In-Mesh Services
Disable TLS in the application. Let Istio own encryption.
App ββplaintextβββΆ Sidecar βββIstio mTLSβββ Sidecar ββplaintextβββΆ App
β Full L7 visibility (metrics, tracing, access logs)
β L7 routing (retries, fault injection, traffic splitting)
β AuthorizationPolicy based on SPIFFE identity
β Zero app-level TLS configuration
β Automatic cert rotation (24h default, no app restart β SDS hot-swap)
Legitimate reasons to keep app-level TLS:
- Compliance β PCI-DSS or HIPAA may not accept βthe mesh handles itβ
- Zero-trust within the pod β though if the sidecar is compromised, the attacker already sees decrypted traffic flowing through it
- Migration β transitioning to Istio, havenβt stripped TLS from apps yet (use
PERMISSIVEduring transition)
DestinationRule TLS Modes
DestinationRule controls how the client-side sidecar connects to the upstream. The spec.trafficPolicy.tls.mode field determines the TLS behavior:
| Mode | What the Client Sidecar Does | Use Case |
|---|---|---|
DISABLE | Sends plaintext. No TLS origination. | Destination has no TLS. Will fail if server PeerAuthentication is STRICT. |
SIMPLE | Originates one-way TLS. Validates server cert, does not present a client cert. | External HTTPS services outside the mesh (e.g., api.stripe.com). |
MUTUAL | Originates mTLS using user-supplied certs (you specify clientCertificate, privateKey, caCertificates). | External services requiring mTLS with specific certs (not Istio-issued). |
ISTIO_MUTUAL | Originates mTLS using Istio-issued SPIFFE certs. Fully auto-managed by istiod. | In-mesh communication. Auto-configured by Istio β rarely set manually. |
# Talking to an external HTTPS API (one-way TLS)
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: external-api
spec:
host: api.stripe.com
trafficPolicy:
tls:
mode: SIMPLE
# Explicit ISTIO_MUTUAL (rarely needed β Istio auto-configures this)
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: my-service-mtls
spec:
host: my-service.default.svc.cluster.local
trafficPolicy:
tls:
mode: ISTIO_MUTUALPeerAuthentication + DestinationRule Interaction
PeerAuthentication controls what the server sidecar accepts. DestinationRule controls what the client sidecar sends. Both must agree:
Client DestinationRule Server PeerAuthentication Result
ββββββββββββββββββββββββ βββββββββββββββββββββββββ βββββββββββββββββββββββ
ISTIO_MUTUAL STRICT β Works (mTLS)
ISTIO_MUTUAL PERMISSIVE β Works (mTLS)
DISABLE STRICT β Fails (server rejects plaintext)
DISABLE PERMISSIVE β Works (plaintext)
ISTIO_MUTUAL DISABLE β Fails (server rejects mTLS)
Port Protocol Detection and TLS
Istio auto-detects port protocols (or uses explicit port naming in Service definitions). This directly affects what happens when the client app sends TLS:
| Port Detection | Envoy Outbound Behavior | Client Sends TLS? | Result |
|---|---|---|---|
Port named http / grpc or auto-detected as HTTP | Envoy expects plaintext HTTP, runs HTTP Connection Manager | Client sends TLS bytes (0x16 0x03 0x03) | Breaks β Envoyβs HTTP parser fails on TLS record header |
Port named https / tls / tcp or unnamed | Envoy treats as opaque TCP | Client sends TLS bytes | Passes through β Envoy tunnels inside Istio mTLS |
Port named "http" + client sends TLS:
Client App ββTLS bytesβββΆ Envoy outbound
β
HTTP parser sees 0x16 0x03 0x03
(TLS record header, not "GET"/"POST")
β parse error β connection reset β
Port named "https" + client sends TLS:
Client App ββTLS bytesβββΆ Envoy outbound
β
TCP proxy mode β pipes bytes through
β tunneled inside Istio mTLS β
(but L7 features lost)
Webhook TLS with Istio (kube-apiserver + Sidecar)
Kubernetes mutating/validating webhooks are called by kube-apiserver over HTTPS. The webhook spec mandates TLS β a caBundle is registered in the MutatingWebhookConfiguration/ValidatingWebhookConfiguration, and kube-apiserver validates the webhook serverβs cert against that CA.
The Problem
kube-apiserver is NOT in the mesh. It runs on control plane nodes, has no Envoy sidecar, has no SPIFFE identity. It speaks regular TLS (one-way), not Istio mTLS.
kube-apiserver Webhook Pod (operator)
(NOT in mesh, ββββββββββββββββββββββββββββββββ
no sidecar) β iptables ββββββββββββββ β
β β redirect β Envoy β β
β Regular HTTPS β ββββββββββββΆβ sidecar β β
β (not Istio mTLS) β β β (:15006) β β
βββββββββββββββββββββββββββββββββββββββββββββΆββββ βββββββ¬βββββββ β
β β localhost β
β Cannot present SPIFFE cert. β β β
β No istio-peer-exchange ALPN. β ββββββββΌββββββββ β
β β β Webhook β β
β β β Server :9443 β β
β β ββββββββββββββββ β
β ββββββββββββββββββββββββββββββββ
When kube-apiserverβs TLS connection arrives at the webhook pod, iptables redirects it to Envoy on :15006. Envoy performs protocol sniffing:
Envoy filter chain matching:
Check: Is this Istio mTLS? (ALPN = istio-peer-exchange)
β kube-apiserver doesn't send this β NO MATCH
PeerAuthentication = STRICT:
β Only Istio mTLS filter chain exists
β No match β CONNECTION REJECTED β
PeerAuthentication = PERMISSIVE:
β Falls to second filter chain (TCP passthrough)
β kube-apiserver's TLS bytes pass through to webhook server
β Webhook server completes TLS handshake directly β
Two Independent PKI Systems
The webhook pod has two completely separate trust chains operating simultaneously:
Istio PKI:
istiod CA β issues SPIFFE SVIDs β Envoy sidecar
Used for: sidecar β sidecar mTLS (mesh traffic)
Webhook PKI:
cert-manager CA (or manual certs) β webhook server cert
Used for: kube-apiserver β webhook TLS
Registered in: MutatingWebhookConfiguration.caBundle
These never interact. Independent trust chains, independent certificates.
Solutions
Option 1: Port-level PeerAuthentication override (recommended)
Keep STRICT for mesh traffic, PERMISSIVE only on the webhook port:
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: my-operator-webhook
namespace: my-operator-ns
spec:
selector:
matchLabels:
app: my-operator
mtls:
mode: STRICT # all other ports: Istio mTLS only
portLevelMtls:
9443:
mode: PERMISSIVE # webhook port: accept non-Istio TLS tooTraffic from mesh services hits STRICT. Traffic from kube-apiserver on :9443 passes through as TCP β kube-apiserverβs TLS handshake reaches the webhook server directly.
Option 2: Exclude the webhook port from sidecar interception
metadata:
annotations:
traffic.sidecar.istio.io/excludeInboundPorts: "9443"iptables skips redirecting port 9443 to Envoy. kube-apiserver connects directly to the webhook server. Downside: no mesh visibility or AuthorizationPolicy on that port.
Option 3: Disable sidecar injection entirely
metadata:
annotations:
sidecar.istio.io/inject: "false"Nuclear option β loses all mesh features for the entire pod.
Egress HTTPS to the Internet with STRICT Mode
When a meshed client app makes an HTTPS request to an external service (e.g., https://api.stripe.com), PeerAuthentication: STRICT does not break it.
Client App ββHTTPSβββΆ Client Envoy ββTLS passthroughβββΆ api.stripe.com
β β
No server-side sidecar. Not in mesh.
STRICT doesn't apply β No PeerAuthentication
there's no remote sidecar to enforce.
to enforce it.
Envoy peeks at SNI, routes
via TCP proxy, passes bytes through.
PeerAuthentication: STRICT only governs sidecar-to-sidecar connections within the mesh. For external destinations, there is no remote sidecar β the client sidecar tunnels the appβs TLS bytes directly to the internet. The appβs TLS handshake completes with the external server.
For L7 control over egress (allow/deny specific external hosts), use ServiceEntry + DestinationRule with mode: SIMPLE β that is routing policy, not PeerAuthentication.
See Also
- Istio Security: mTLS, SPIFFE, AuthorizationPolicy β mTLS mechanics, PeerAuthentication policy, AuthorizationPolicy
- Istio Architecture Deep Dive β Sidecar injection, iptables interception, xDS, ambient mode
- Istio Traffic Management β VirtualService, DestinationRule, Gateway API, ServiceEntry
- TLS 1.3 Handshake β The TLS protocol underlying both app TLS and Istio mTLS
- HTTP Sessions & Cookie Security β Application-layer auth that rides on top of these transport layers
Interview Prep
Q: A pod has both app-level TLS and Istio mTLS enabled. Walk through what happens on each network segment.
A:
Leg 1 (client app β client sidecar):
Plaintext on localhost. iptables redirects to Envoy :15001.
Envoy reads original destination via SO_ORIGINAL_DST.
Leg 2 (client sidecar β server sidecar):
Istio mTLS. Both sidecars present SPIFFE X.509 SVIDs.
TLS 1.3, ECDHE + AES-256-GCM. Encrypted on the wire.
If client sent app-TLS bytes, they are tunneled inside
this mTLS connection (double encryption on the wire).
Leg 3 (server sidecar β server app):
Sidecar terminates Istio mTLS. Forwards inner bytes to
localhost. If inner bytes are app TLS and app expects TLS,
the app completes its own TLS handshake. If app expects
plaintext, it fails.
The recommended approach is to disable app TLS entirely for in-mesh services and let Istio handle encryption. This gives full L7 visibility and avoids redundant crypto overhead.
Q: Mesh-wide PeerAuthentication is STRICT. Your operatorβs validating webhook stops working. Why, and how do you fix it?
A: kube-apiserver is not in the mesh β it has no sidecar and no SPIFFE identity. It speaks regular TLS, not Istio mTLS. When its TLS connection hits the webhook podβs sidecar, Envoy checks for the istio-peer-exchange ALPN and doesnβt find it. In STRICT mode, only the Istio mTLS filter chain exists β the connection is rejected.
Fix with port-level override:
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: webhook-auth
spec:
selector:
matchLabels:
app: my-operator
mtls:
mode: STRICT
portLevelMtls:
9443:
mode: PERMISSIVE # accept kube-apiserver's regular TLSThis keeps STRICT for all mesh traffic while allowing the webhook port to accept non-Istio TLS from kube-apiserver.
Q: Your client app is hardcoded to use https:// for an in-mesh service. The service port is named http. What breaks and why?
A: The client app sends TLS bytes (starting with 0x16 0x03 0x03 β TLS record header) to its sidecar. Because the port is named http, the client-side Envoyβs filter chain uses the HTTP Connection Manager, which expects plaintext HTTP. The HCM tries to parse the TLS record header as an HTTP method β fails immediately. Connection reset.
This breaks on the client side before traffic even reaches the wire. PeerAuthentication mode is irrelevant β the issue is protocol mismatch at the outbound Envoy.
Fix: either rename the port to https/tcp (Envoy treats it as opaque TCP, but you lose L7 features), or β the better fix β remove https:// from the client and let it send plaintext HTTP so Istio can handle encryption transparently.
Q: An in-mesh client sends HTTPS to an external API (api.stripe.com). PeerAuthentication is STRICT. Does it break?
A: No. PeerAuthentication: STRICT only governs sidecar-to-sidecar connections within the mesh. External destinations have no sidecar β there is no PeerAuthentication to enforce. The client sidecar peeks at the SNI from the TLS ClientHello, routes the connection via TCP proxy, and passes the appβs TLS bytes through to the internet. The TLS handshake completes directly between the client app and the external server.
Q: Compare DestinationRule TLS modes: DISABLE, SIMPLE, MUTUAL, ISTIO_MUTUAL.
A:
Mode Client sidecar behavior Typical use case
ββββββββββββββ ββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββ
DISABLE Sends plaintext, no TLS Dest has no TLS, or
explicitly disabling mTLS
SIMPLE One-way TLS: validates server External HTTPS service
cert, no client cert presented (api.stripe.com, etc.)
MUTUAL mTLS with user-supplied certs External service requiring
(you specify cert/key/CA paths) mTLS with specific certs
ISTIO_MUTUAL mTLS with auto-managed SPIFFE In-mesh service-to-service
certs from istiod (auto-configured by Istio,
rarely set manually)
ISTIO_MUTUAL is what Istio auto-configures for in-mesh traffic. You almost never write a DestinationRule with this mode unless overriding a specific port or subset.
Q: Why canβt kube-apiserver just use Istio mTLS for webhooks?
A: Three reasons:
- kube-apiserver runs outside the mesh. Even on managed Kubernetes (GKE, EKS), the control plane has no sidecar and no SPIFFE certificate.
- The webhook spec hardcodes regular TLS.
caBundleis how trust is established β thereβs no extension point for βuse Istio mTLS instead.β - Separate PKI. kube-apiserver uses the cluster CA or a dedicated webhook CA (often cert-manager). Istioβs SPIFFE trust chain is completely separate. The two PKI systems coexist on the same pod but never interact.