The team deploys Istio, sees mTLS metrics in Kiali, and marks the zero-trust item done. Six months later, a compromised init container with sidecar injection disabled makes plaintext HTTP calls across the cluster without presenting a single certificate.
Deploying Istio does not mean mTLS is enforced. The default PERMISSIVE mode accepts plaintext connections alongside encrypted ones when no PeerAuthentication is configured. Any workload without a sidecar reaches internal services as if the mesh were not present. Application-level certificate bypass makes the problem worse even when the mode is STRICT.
mTLS Is Peer Authentication, Not Just Encryption
The security value of mTLS over TLS is identity: each side proves who it is via certificate chain. Service meshes that accept plaintext connections abandon that identity guarantee entirely for non-meshed callers.
Server-only TLS proves the server identity to the client. mTLS additionally proves the client identity to the server. Removing one side eliminates peer authentication, turning the encrypted channel into encryption without authentication.
Istio represents workload identity as SPIFFE SVIDs, X.509 certificates tied to Kubernetes ServiceAccount, not hostname. This distinction matters: IP-based authentication fails in environments where pods are rescheduled frequently. SVIDs survive IP rotations and pod restarts.
Linkerd takes a different stance. It issues 24-hour workload certificates per proxy, verified automatically on every connection. In the Linkerd model, identity is non-negotiable and PERMISSIVE does not exist as a mode. In Istio's PERMISSIVE mode, connections with no client certificate are accepted, source principal is empty, and AuthorizationPolicy rules checking that field match against nothing.
PERMISSIVE Is the Default, and Teams Never Leave It
PERMISSIVE mode was designed as a temporary migration state for gradual rollout. It became the permanent configuration for most deployments because no alert fires and Kiali reports mTLS traffic without distinguishing encrypted from plaintext connections.
PERMISSIVE mode is the correct starting point during a brownfield migration to the mesh. The risk is not PERMISSIVE mode existing: it is PERMISSIVE mode persisting with no enforcement deadline. A namespace-by-namespace rollout to STRICT, with PeerAuthentication objects applied and validated before moving to the next namespace, is the standard exit ramp.
The Istio docs are direct: UNSET mode inherits from parent. The mesh-level default when no PeerAuthentication exists is PERMISSIVE, not STRICT. In a freshly installed Istio cluster with no additional configuration, all services accept plaintext.
PERMISSIVE mode sidecars upgrade mTLS connections transparently, but plaintext passes through silently. Mixed traffic appears in dashboards as "mTLS traffic," but the proportion of plaintext connections is not flagged by default. Kiali displays the lock icon because the mesh is present, not because all traffic is encrypted.
The Istio migration guide recommends moving to STRICT after rollout. No automated enforcement or expiry mechanism exists in the platform. Tetrate's TCA rule TIS0208 flags permissive namespaces specifically because teams stay in this state for months, sometimes years.
The Attack Path: Plaintext from a Sidecar-less Pod
An attacker in any pod without a sidecar reaches any PERMISSIVE service with a plain HTTP connection. The response arrives with no certificate presented and source identity empty in the request context.
Sidecar injection is disabled via the sidecar.istio.io/inject: "false" annotation or the istio-injection=disabled namespace label. Both create unsidecarred pods. System pods like debugging tools, migration jobs, and monitoring containers frequently carry this annotation by design.
Without a sidecar, the iptables rules that redirect traffic through Envoy are not installed. The application's outbound TCP goes directly to the destination IP, bypassing the proxy entirely. The destination sidecar in PERMISSIVE mode accepts the plaintext connection and forwards to the workload with no TLS handshake.
The source.principal in the request context is empty. AuthorizationPolicy deny rules checking source.principal miss this traffic unless they explicitly deny empty principals:
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: deny-unauthenticated
spec:
action: DENY
rules:
- from:
- source:
notPrincipals: ["*"]
Without this explicit denial of empty principals, AuthorizationPolicy rules that assume source identity provide zero protection for PERMISSIVE services.
(Istio ambient mode eliminates per-pod iptables injection in favor of a node-level ztunnel; the enforcement gap differs and is not covered here.)
STRICT Mode Is Not Enough: Certificate Verification Bypass in Application Code
Even when PeerAuthentication is STRICT and Envoy performs a TLS handshake, application code that disables certificate validation defeats peer authentication. The channel is encrypted, but any certificate from any workload is accepted without chain verification.
In Go, tls.Config{InsecureSkipVerify: true} disables all certificate chain and hostname validation. Any certificate is accepted, enabling MITM attacks within the mesh. The attacker needs no valid credential, only a network position:
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
In Python, requests.get(url, verify=False) disables cert verification. A requests.Session with this flag propagates the bypass to all subsequent connections in the same pool. This pattern spreads through copy-pasting internal snippets that never reached code review.
This pattern is common in internal services that treat the mesh as implicit trust. Developers assume Envoy verified the peer and skip application-level validation. CWE-297 captures this class, distinct from mesh-level bypass but equally impactful on east-west trust.
CVE-2022-21654: When mTLS Assumptions Break at the Protocol Level
CVE-2022-21654 shows that even correctly-configured STRICT mTLS in Istio can be bypassed. Sessions are reused without re-validation after policy changes, allowing connections that should be rejected.
The CVE carries CVSS 7.3 (High) and CWE-367 (TOCTOU race condition). It affects Istio 1.11.x, 1.12.x, and 1.13.x, patched in releases 1.11.7, 1.12.4, and 1.13.1. The vulnerability window occurs between cert rotation and closure of existing connections. Keepalive connections with the old identity continue being accepted after the identity is revoked.
Red Hat advisory RHSA-2022:1275 states it directly: incorrect configuration handling allows mTLS session reuse without re-validation after validation settings have changed. ISTIO-SECURITY-2022-004 (patched in 1.11.8, 1.12.5, 1.13.2) covers CVE-2022-21657: Envoy accepts X.509 certificates with incorrect Extended Key Usage fields. An e-mail certificate (id-kp-emailProtection) passes peer authentication where id-kp-clientAuth is required. The vulnerability window overlaps but requires a separate patch cycle.
Detection and Enforcement: What Actual STRICT Requires
Real mTLS enforcement needs 3 conditions simultaneously: a mesh-wide STRICT PeerAuthentication, 100% sidecar coverage enforced at admission, and no namespace policy overriding STRICT with PERMISSIVE. Any gap creates a complete bypass.
Mesh-wide STRICT requires a PeerAuthentication in namespace istio-system with no selector and mtls.mode: STRICT. Namespace policies override the mesh-wide policy entirely when they specify a different mode. A single namespace with mode: PERMISSIVE nullifies the mesh-wide STRICT for all services inside it.
# Check PeerAuthentication configuration across all namespaces
kubectl get peerauthentication -A
# Analyze configuration gaps and conflicts before enforcing STRICT
istioctl analyze --all-namespaces
istioctl analyze reports PeerAuthentication conflicts and namespaces where sidecars are missing before STRICT is applied. Pods without sidecars in a STRICT namespace have connections rejected, which is the correct failure mode. But this only works if sidecar injection is enforced at the admission controller. A STRICT namespace with a sidecar-less pod (via annotation or namespace label) is a silent exception the dashboard does not show.
Detecting the Gap from Outside: mTLS Posture in Security Tooling
The PERMISSIVE gap is detectable by probing: a service that responds to both a mTLS connection and a plaintext connection is not enforcing peer authentication. This holds regardless of what the mesh configuration declares.
The MAGO Intel tool (intel.mago.team) probes internal service endpoints with both mTLS and plaintext connections. It identifies services that accept plaintext in what should be a STRICT mesh, without requiring access to the cluster's policy API. This surfaces actual enforcement state rather than declared policy state.
Services that respond to mTLS probes but reject plaintext are correctly STRICT. Services that respond to both are PERMISSIVE. Correlating probe results with service mesh inventory identifies which services are outside the mesh entirely versus in the wrong mode.
Real mTLS enforcement has 4 checkpoints: STRICT PeerAuthentication mesh-wide, sidecar injection enforced at admission, no InsecureSkipVerify or verify=False in code, and Envoy past these CVEs. Auditing all 4 takes under an hour. Skipping the audit means the "mTLS enabled" dashboard metric is a false confidence signal.
Top comments (0)