All Articles

What is mTLS? Mutual TLS Explained Simply (with Kubernetes Examples)

mTLS means both sides of a connection verify each other's identity. It's the backbone of zero-trust networking in Kubernetes service meshes. Here's how it works in plain language.

DevOpsBoysApr 9, 20265 min read
Share:Tweet

You've heard of HTTPS. mTLS is the next step — instead of only the server proving its identity, both sides prove who they are.

It's the foundation of zero-trust networking, and it's how service meshes like Istio secure communication between microservices.


Regular TLS First (Quick Recap)

When you visit https://devopsboys.com:

  1. Your browser connects to the server
  2. Server sends its TLS certificate — "I am devopsboys.com, here's proof"
  3. Your browser verifies the certificate against trusted Certificate Authorities
  4. Encrypted connection established

You (the client) are anonymous. The server has no idea who you are — it trusts anyone who connects. This is fine for public websites.


The Problem in Microservices

Imagine you have 20 microservices talking to each other inside a Kubernetes cluster:

frontend → auth-service → user-service → database-service

With regular TLS (or no TLS), user-service can't verify:

  • Is this request actually from auth-service?
  • Or is it from a compromised pod pretending to be auth-service?
  • Or from a malicious container that got deployed somehow?

If any service is compromised, it can call any other service freely. No checks.


What mTLS Adds

mTLS = Mutual TLS. Both client and server present and verify certificates.

auth-service → "I am auth-service, here's my certificate"
user-service → "I am user-service, here's MY certificate"
Both sides verify each other → encrypted + authenticated connection

Now user-service knows for certain: this request is from auth-service, signed by a certificate authority I trust. Not from a random pod.


How mTLS Works Step by Step

1. auth-service initiates connection to user-service

2. user-service sends its certificate
   → auth-service verifies: "valid cert, issued by cluster CA, is it really user-service? yes"

3. auth-service sends its certificate  
   → user-service verifies: "valid cert, issued by cluster CA, is it really auth-service? yes"

4. Both sides agree on encryption keys

5. All traffic encrypted + both sides authenticated

The key: every service has its own certificate issued by a trusted Certificate Authority (CA) that the cluster controls.


What's Inside These Certificates

In Kubernetes service meshes, pod certificates follow the SPIFFE standard (Secure Production Identity Framework For Everyone).

Each pod gets a certificate called an SVID (SPIFFE Verifiable Identity Document):

spiffe://cluster.local/ns/production/sa/auth-service

This encodes:

  • Cluster: cluster.local
  • Namespace: production
  • Service Account: auth-service

So when user-service receives a connection, it doesn't just verify "is the cert valid" — it verifies who the cert belongs to. It can enforce: "I will only accept connections from spiffe://cluster.local/ns/production/sa/auth-service."


Without a Service Mesh: Manual mTLS

You can implement mTLS in your application code without a service mesh. Using cert-manager to issue certificates + configuring your app's TLS settings:

Go example:

go
// Load your certificate and key
cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
 
// Load the CA that signed client certs
caCert, _ := os.ReadFile("ca.crt")
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
 
// Configure TLS to require client certificates
tlsConfig := &tls.Config{
    Certificates: []tls.Certificate{cert},
    ClientAuth:   tls.RequireAndVerifyClientCert,  // this is the mTLS part
    ClientCAs:    caCertPool,
}
 
server := &http.Server{
    TLSConfig: tlsConfig,
}

Problem with manual mTLS: every service needs to be configured, certificates need rotation, each language/framework has different TLS APIs. At 20 services, this becomes painful.


With a Service Mesh: Automatic mTLS (the Easy Way)

This is why service meshes exist. With Istio or Linkerd, mTLS is automatic — no code changes needed.

Istio mTLS:

yaml
# Enable STRICT mTLS for the entire namespace
# (reject all non-mTLS traffic)
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: production
spec:
  mtls:
    mode: STRICT

That's it. Now every pod-to-pod connection in the production namespace automatically:

  • Has a certificate injected by Istio's sidecar (Envoy)
  • Verifies the other side's certificate
  • Encrypts all traffic

The app code doesn't change at all. Istio handles the full TLS handshake at the sidecar level.

Istio Authorization Policy — who can call whom:

yaml
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: user-service-policy
  namespace: production
spec:
  selector:
    matchLabels:
      app: user-service
  action: ALLOW
  rules:
    - from:
        - source:
            principals:
              - "cluster.local/ns/production/sa/auth-service"
      to:
        - operation:
            methods: ["GET", "POST"]
            paths: ["/api/users/*"]

This says: only auth-service (identified by its SPIFFE certificate) can call user-service on /api/users/*. Everything else is denied.


mTLS with Linkerd (Even Simpler)

Linkerd automatically enables mTLS for all meshed pods with zero configuration. It uses Rust-based micro-proxies (much lighter than Envoy).

bash
# Install Linkerd
linkerd install --crds | kubectl apply -f -
linkerd install | kubectl apply -f -
 
# Inject Linkerd into a namespace
kubectl annotate namespace production linkerd.io/inject=enabled
 
# Verify mTLS is active
linkerd viz tap deploy/auth-service -n production
# Output shows: tls=true for all connections

No PeerAuthentication YAML needed. mTLS is on by default.


Certificate Rotation

A big advantage of service mesh mTLS: certificates are rotated automatically.

Manually managing certificates means:

  • Certificates expire → services break
  • Someone forgets to renew → production outage
  • Rotation requires touching every service

Istio (via istiod) issues short-lived certificates (default: 24 hours) and rotates them automatically. Pods get new certs before the old ones expire. Zero manual work.


mTLS vs Regular HTTPS: Summary

HTTPSmTLS
Server authenticated
Client authenticated
Traffic encrypted
Use casePublic web trafficService-to-service (internal)
Who manages certsLet's Encrypt / ACMCluster CA (Istio/Linkerd)
Zero-trust capableNoYes

When Do You Need mTLS?

You need mTLS when:

  • Compliance (PCI-DSS, HIPAA, SOC2) requires encryption of internal traffic
  • Zero-trust: you want to enforce "service A can only talk to service B" at the network level
  • Sensitive data (payment info, health records) flows between services
  • Your cluster is multi-tenant (different teams share one cluster)

You might not need mTLS if:

  • Small internal app, single namespace, single team
  • No compliance requirements
  • NetworkPolicy is sufficient for your security needs
  • Service mesh overhead (latency, CPU, complexity) isn't worth it yet

The Bottom Line

mTLS = both sides show ID. Think of it like showing your badge entering a building AND the building guard showing their ID to you.

For Kubernetes: start with NetworkPolicy for basic isolation. Add a service mesh (Istio or Linkerd) for automatic mTLS when you need zero-trust or compliance. The service mesh does all the hard work — your apps don't need to change a line of code.


Related: How to Set Up Istio Service Mesh | DevSecOps Pipeline Guide

Newsletter

Stay ahead of the curve

Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.

Related Articles

Comments