Safeguard
Identity Security

How to set up mutual TLS (mTLS) between microservices

A step-by-step guide to configuring mTLS across microservices with Istio — from CA setup and PeerAuthentication policies to certificate rotation and verification.

Karan Patel
Cloud Security Engineer
6 min read

Microservices talk to each other constantly, and by default most of those calls are unauthenticated and unencrypted at the transport layer. A compromised pod, a misconfigured sidecar, or a rogue workload inside the cluster can impersonate any service and read or tamper with traffic in transit. If you need to set up mutual TLS microservices communication, you're solving two problems at once: encrypting data in transit and proving that both the client and the server are who they claim to be — not just one direction, as with standard TLS.

This guide walks through a practical, production-ready path to deploying mTLS across a microservices fleet, including mtls service mesh configuration in Istio, certificate issuance and rotation, and how to verify enforcement instead of just assuming it's working. By the end, you'll have workload identities backed by short-lived certificates, mesh-wide policies that reject plaintext traffic, and a repeatable process your team can audit.

Step 1: Map Your Trust Domain and Service Inventory

Before touching any config, define the boundaries of trust. mTLS is only as strong as the certificate authority (CA) issuing identities, so decide:

  • Trust domain: a single root CA per environment (dev, staging, prod), or a hierarchy with an offline root and per-cluster intermediates.
  • Service inventory: every workload that will participate, including batch jobs, sidecars, and external gateways that terminate TLS at the edge.
  • Identity format: SPIFFE IDs (spiffe://trust-domain/ns/namespace/sa/service-account) are the de facto standard and what Istio uses natively.

Document this in a short design note before implementation — it's the artifact you'll need later when an auditor or a new engineer asks "why does service A trust service B?"

Step 2: Set Up Mutual TLS Microservices Using a Private CA

You need a CA that issues short-lived certificates automatically; hand-rolled, long-lived certs are an operational and security liability. Two common paths:

Option A — cert-manager with Istio CSR agent (Kubernetes-native):

helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager --create-namespace \
  --set installCRDs=true

kubectl apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: istio-ca
spec:
  ca:
    secretName: istio-ca-key-pair
EOF

Option B — Istio's built-in istiod CA, which is the fastest way to get workload certificates issued and rotated without standing up a separate PKI:

istioctl install --set profile=default \
  --set values.pilot.env.PILOT_CERT_PROVIDER=istiod

Either way, verify the root of trust is stored somewhere that isn't the same failure domain as your workload cluster — a leaked or lost root CA means re-issuing every certificate in the mesh.

Step 3: Configure mTLS Service Mesh Settings in Istio

With a CA in place, the mtls service mesh configuration itself is largely declarative. Install Istio with mTLS-aware defaults and confirm the sidecar injector is active on your namespaces:

kubectl label namespace payments istio-injection=enabled
kubectl label namespace orders istio-injection=enabled

Each pod now gets an Envoy sidecar that handles certificate presentation and validation transparently — your application code doesn't need to know TLS is happening.

Step 4: Complete the Istio mTLS Setup with PeerAuthentication and DestinationRules

Installing Istio doesn't enforce mTLS by itself; it defaults to PERMISSIVE mode so plaintext traffic still works during migration. To finish the istio mtls setup and require encrypted, authenticated traffic mesh-wide:

apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
  name: default
  namespace: istio-system
spec:
  mtls:
    mode: STRICT

For service-to-service calls that need explicit routing rules (rather than mesh-wide defaults), pair this with a DestinationRule:

apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: orders-mtls
  namespace: orders
spec:
  host: orders.orders.svc.cluster.local
  trafficPolicy:
    tls:
      mode: ISTIO_MUTUAL

Roll this out namespace by namespace, not cluster-wide in one shot. Flip PERMISSIVE to STRICT on a low-traffic namespace first, watch error rates, then expand.

Step 5: Automate mTLS Certificate Management and Rotation

Manual certificate handling is where most mTLS deployments quietly fail months later. Solid mtls certificate management means:

  • Short lifetimes: Istio's default workload certificate TTL is 24 hours, renewed automatically by istiod — don't extend this without a strong reason.
  • Monitoring expiry: alert on certificates nearing expiry that aren't auto-rotating, which usually signals a stuck sidecar or a workload outside the mesh.
  • CA rotation drills: practice rotating the root/intermediate CA in staging at least twice a year so it's not a first-time event during an incident.
  • Non-mesh workloads: VMs, serverless functions, and third-party services that can't run a sidecar need an explicit plan — often a SPIFFE-compatible agent or a gateway that terminates mTLS on their behalf.

Step 6: Verify Enforcement and Troubleshoot Common Failures

Configuration without verification is a false sense of security. Confirm enforcement with:

istioctl authn tls-check <pod-name>.<namespace>

This should report STRICT mode and a matching client/server certificate chain for every workload. Also test the negative case — a plaintext client should be rejected:

kubectl exec -it debug-pod -- curl -v http://orders.orders.svc.cluster.local

Common issues and fixes:

  • "Connection reset by peer" after enabling STRICT mode: usually a workload outside the mesh (no sidecar) still calling in plaintext. Check istioctl proxy-status for NOT SENT or STALE entries.
  • Certificate chain validation errors: often a stale root CA cached in an old sidecar after a CA rotation. Restart affected pods to force a fresh CSR.
  • Intermittent 503s during rollout: a PeerAuthentication policy in STRICT mode reaching a namespace before sidecar injection has propagated. Sequence the rollout so injection lands first, confirm with istioctl proxy-status, then apply the policy.
  • Traffic silently bypassing mTLS: check for DestinationRule conflicts — a rule set to DISABLE at a narrower scope can override a mesh-wide STRICT policy.
  • Legacy services failing entirely: build an explicit exception list rather than reverting to PERMISSIVE mesh-wide; that reintroduces the exact gap you were closing.

Run these checks after every mesh upgrade, not just at initial rollout — Istio version bumps have historically shipped changes to default TLS behavior.

How Safeguard Helps

Getting the YAML right is only half the job — the harder problem is knowing whether your mTLS posture holds up over time as services, namespaces, and CAs change hands across teams. Safeguard continuously inventories service identities and certificate issuance paths across your clusters, flags namespaces silently running in PERMISSIVE mode, and surfaces certificates approaching expiry or signed by an unexpected CA before they cause an outage or a trust gap. Instead of relying on tribal knowledge or a one-time audit, teams get a live, auditable view of their software supply chain's identity layer — which pairs naturally with the access controls and provenance checks Safeguard already applies to your build and deployment pipeline. If you've just finished a mesh-wide mTLS rollout, that's exactly the moment to plug in continuous verification rather than trusting that today's configuration stays correct tomorrow.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.