
If you run a service mesh like Istio but haven't touched PeerAuthentication, there's a good chance every pod-to-pod call inside your cluster is still going out in plaintext. Istio can encrypt and authenticate that traffic automatically, with no code changes, but it won't do it until you tell it to.
What mTLS actually buys you
Mutual TLS (mTLS) means both sides of a connection prove their identity with a certificate, not just the server. In a Kubernetes cluster, that matters because the default state of most clusters is: any pod can talk to any other pod's IP and port, over plain HTTP, with no proof of who it really is. Network policies can restrict which pods may connect, but they don't stop a compromised pod from impersonating another service's identity on the wire.
Istio solves this at the network layer instead of the application layer. Every pod in the mesh gets an Envoy sidecar proxy injected next to it. That sidecar intercepts all inbound and outbound traffic. When mTLS is enabled, the sidecar automatically wraps outgoing connections in TLS using a short-lived certificate issued by Istio's certificate authority (istiod), and the receiving sidecar verifies that certificate before passing the request to your application. Your application code never sees any of this — it still just sends plain HTTP or gRPC to localhost.
The two knobs: PeerAuthentication and DestinationRule
Istio controls mTLS with two separate CRDs, and conflating them is the single most common source of confusion:
PeerAuthentication— controls what the server side of a connection will accept: plaintext, mTLS, or both.DestinationRule— controls what the client side will send when it initiates a connection.
In a vanilla Istio install, the global default is usually PERMISSIVE mode: sidecars accept both plaintext and mTLS, so you can roll the mesh out gradually without breaking traffic from pods that don't have a sidecar yet. Once every workload has a sidecar, you move to STRICT, which refuses plaintext entirely.
Checking your current state
Before changing anything, confirm sidecars are actually injected and see what mode you're in today.
# Confirm the namespace is labeled for automatic sidecar injection
kubectl get namespace default --show-labels
# Label it if it isn't already
kubectl label namespace default istio-injection=enabled
# Confirm pods actually have 2/2 containers (app + istio-proxy)
kubectl get pods -n default
# Ask Istio directly what mTLS mode a specific pod sees
istioctl x describe pod my-app-7f8c9d-xyz
The istioctl x describe output is the fastest way to see the effective policy for a workload, because it merges mesh-wide, namespace-wide, and workload-specific rules into one answer.
Step 1: Turn on mesh-wide STRICT mTLS
A PeerAuthentication resource named default in the istio-system namespace sets the mesh-wide default. This affects every namespace that doesn't have its own override.
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: default
namespace: istio-system
spec:
mtls:
mode: STRICT
Apply it with:
kubectl apply -f peerauth-mesh-strict.yaml
Going straight to a mesh-wide STRICT policy is risky in a live cluster, because any workload that is not in the mesh — no sidecar, a legacy VM, a health-check from outside the mesh — will immediately lose the ability to talk to meshed services in plaintext. That's why most teams scope the rollout per namespace first.
Step 2: Roll out namespace by namespace
A namespace-scoped PeerAuthentication overrides the mesh default for everything in that namespace, which lets you convert one team's workloads at a time.
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: default
namespace: payments
spec:
mtls:
mode: STRICT
If one specific port needs to stay plaintext — for example, a legacy health-check endpoint that an external load balancer hits directly — you can carve out an exception at the port level inside the same resource:
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: default
namespace: payments
spec:
mtls:
mode: STRICT
portLevelMtls:
8080:
mode: PERMISSIVE
Step 3: Tell clients to actually use TLS
Enabling STRICT mode on the server side means sidecars will reject plaintext, but by default Istio's automatic mTLS already configures client sidecars to originate mTLS for any destination inside the mesh — you don't usually need to write a DestinationRule yourself for this. You only need one when you want to be explicit, or when you're calling a workload through a ServiceEntry for something outside the mesh and want to force mTLS there too:
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
name: payments-api-mtls
namespace: payments
spec:
host: payments-api.payments.svc.cluster.local
trafficPolicy:
tls:
mode: ISTIO_MUTUAL
ISTIO_MUTUAL tells the client sidecar to use the certificates Istio itself issues and manages — this is different from MUTUAL, which expects you to supply your own certificate files, and from SIMPLE, which is one-way TLS with no client certificate at all.
Verifying it actually worked
Don't take the YAML's word for it. Confirm traffic is actually encrypted:
# Check the authentication policy Istio computed for a workload
istioctl x describe pod payments-api-7d9f-abcd
# Tap the sidecar's own stats for TLS handshake counters
kubectl exec -n payments payments-api-7d9f-abcd -c istio-proxy -- \
pilot-agent request GET stats | grep ssl_handshake
# Try a raw plaintext curl from a non-meshed pod into a STRICT workload — it should fail
kubectl run curl-test --rm -it --image=curlimages/curl -- \
curl -v http://payments-api.payments.svc.cluster.local
If that last command still succeeds with a 200, the workload isn't actually enforcing STRICT yet — check whether the PeerAuthentication landed in the right namespace, and whether the pod has picked up the updated config (sidecars pull config from istiod, so a brand-new policy can take a few seconds to propagate).
Common pitfalls
- Forgetting non-meshed callers. Cron jobs, external health checks, or pods in namespaces without injection will break the moment you flip to
STRICTif they talk in plaintext. - Mixing up client and server config. A
PeerAuthenticationinSTRICTmode with no matchingDestinationRuleoverride is usually fine, because automatic mTLS handles the client side — but a customDestinationRulethat setstls.mode: SIMPLEor disables TLS explicitly will silently break things even if the server demands mTLS. - Applying the mesh-wide policy first. Always roll out namespace-by-namespace, verify, then tighten the mesh-wide default last, once every namespace has its own explicit policy or sidecar coverage.
- Assuming mTLS means authorization. mTLS proves identity, not permission. A service can still call another service it has no business calling. You need
AuthorizationPolicyresources on top of mTLS to actually restrict who can call whom.
Troubleshooting
If requests start failing with connection resets or 503 UF (upstream failure) errors in Envoy's access logs right after you change a PeerAuthentication, the usual culprits are:
- A caller outside the mesh (no sidecar) trying to reach a
STRICTworkload in plaintext. - A stale sidecar config — check propagation with
istioctl proxy-status, which shows whether each proxy is in sync (SYNCED) withistiod. - A
DestinationRulesomewhere upstream that pinstls.modeto something other thanISTIO_MUTUAL, overriding the automatic behavior.
istioctl proxy-status
istioctl proxy-config listener payments-api-7d9f-abcd -n payments
Where to go next
Once STRICT mTLS is enforced mesh-wide, the natural next step is AuthorizationPolicy — defining exactly which service identities may call which other services, instead of relying on "anything in the mesh can reach anything else." That turns mTLS from "everything is encrypted" into "everything is encrypted and only the right things can talk to each other," which is the real end state most teams are aiming for.

Experienced DevOps Engineer proficient in Python, Docker, Kubernetes, CI/CD, and cloud technologies. Specialized in automating processes and building scalable infrastructure for seamless development and deployment.


