Mutual TLS with Istio Service Mesh
Istio: The Service Mesh Approach
Manual mTLS vs Service Mesh
| Approach | Certificate management | App changes needed | Scales to 100 services? |
|---|---|---|---|
| cert-manager + nginx | You manage each cert | Yes — nginx config, volume mounts | No |
| Istio service mesh | Automatic, rotated every 24h | None — sidecar handles it | Yes |
Istio injects an Envoy proxy sidecar into every pod. The sidecar intercepts all traffic and handles mTLS transparently. Your application code never changes.
Install Istio
Istio is being installed in the background as the playground starts. This takes 2-3 minutes.
Wait for istiod to be ready:
kubectl wait --for=condition=Ready pods --all -n istio-system --timeout=180s
kubectl get pods -n istio-system
Expected:
NAME READY STATUS RESTARTS AGE
istiod-xxxxx-yyy 1/1 Running 0 2m
Enable Automatic Sidecar Injection
Label a namespace and Istio automatically injects the Envoy proxy into every pod deployed there.
kubectl create namespace istio-demo
kubectl label namespace istio-demo istio-injection=enabled
kubectl get namespace istio-demo --show-labels
Expected: istio-injection=enabled in the labels.
Deploy Two Services Into the Mesh
We'll use httpbin (an HTTP echo service) as the server and sleep (a minimal curl-capable pod) as the client. No TLS configuration needed anywhere.
# Server: httpbin
kubectl apply -n istio-demo -f \
https://raw.githubusercontent.com/istio/istio/release-1.20/samples/httpbin/httpbin.yaml
# Client: sleep (a simple pod with curl)
kubectl apply -n istio-demo -f \
https://raw.githubusercontent.com/istio/istio/release-1.20/samples/sleep/sleep.yaml
kubectl wait --for=condition=Ready pods --all -n istio-demo --timeout=120s
echo "Services deployed."
Verify the sidecars were injected
kubectl get pods -n istio-demo
Expected: Each pod shows 2/2 READY — that's the app container + the Envoy sidecar.
kubectl describe pod -n istio-demo -l app=httpbin | grep "istio-proxy"
Enforcing STRICT mTLS
mTLS is Permissive by Default
By default Istio runs in PERMISSIVE mode — it accepts both plaintext and mTLS connections. This allows gradual adoption. For production, switch to STRICT mode: no sidecar, no connection.
Apply PeerAuthentication STRICT
One resource, three lines of YAML:
cat > peer-auth.yaml << 'EOF'
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: istio-demo
spec:
mtls:
mode: STRICT
EOF
kubectl apply -f peer-auth.yaml
echo "STRICT mTLS enforced."
Verify mTLS is Working (Transparently)
The sleep pod already has a sidecar. Communication to httpbin works — Istio handles the TLS handshake invisibly.
CLIENT=$(kubectl get pod -n istio-demo -l app=sleep -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n istio-demo $CLIENT -c sleep -- \
curl -s http://httpbin:8000/get | python3 -m json.tool | head -15
Expected: A JSON response — the sleep pod reached httpbin over mTLS. No --cert flags, no key files. Istio handled everything.
Show What Happens Without the Sidecar
In STRICT mode, Istio's Envoy proxy rejects plaintext connections. A pod outside the mesh — no sidecar — cannot connect.
# Run a curl pod in the 'default' namespace (no istio-injection label)
kubectl run non-mesh-test \
--image=curlimages/curl:8.5.0 \
--restart=Never \
-i --rm \
-n default \
-- curl -v --max-time 5 http://httpbin.istio-demo.svc.cluster.local:8000/get \
2>&1 | tail -5
Expected:
* Connection #0 to host httpbin.istio-demo.svc.cluster.local left intact
curl: (56) Recv failure: Connection reset by peer
Or a timeout — the Envoy sidecar on httpbin drops the plaintext connection because it has no client certificate.
mTLS at Scale
| Feature | Manual cert-manager | Istio |
|---|---|---|
| Cert issuance | Per service, manual | Automatic for all pods |
| Cert rotation | Manual or via cert-manager | Every 24h, zero downtime |
| App changes | nginx config, volume mounts | None |
| Enforcement | Per-service config | Namespace or mesh-wide |
| Observability | nginx logs | Kiali, Jaeger, Prometheus built-in |
Cleanup
kubectl delete namespace istio-demo
helm uninstall istiod -n istio-system
helm uninstall istio-base -n istio-system
kubectl delete namespace istio-system
- Previous lesson
- Kubernetes Network Policy
- Next lesson
- RBAC — Least Privilege