Core Idea
Kubernetes does not enforce mTLS between pods, so layer it on: a service mesh is the recommended option, manual certificates or cert-manager the DIY ones, and SPIFFE-backed identity beats plain certs.
- Three options compared: service mesh (recommended), manual mTLS with Kubernetes Secrets, and cert-manager issuers.
- mTLS with verified identity: plain mTLS versus mTLS with SPIFFE, and why the distinction matters.
- mTLS set against other auth methods, plus the noteβs warning that none of this is built in.
mTLS in Kubernetes
Warning
Not Built-in Kubernetes does not natively enforce mTLS between pods. It must be layered on top via a Service Mesh or manual certificate management.
flowchart TD
subgraph PodA["Pod A"]
AppA["App Container"]
SidecarA["Sidecar Proxy(Envoy / Linkerd)"]
end
subgraph PodB["Pod B"]
SidecarB["Sidecar Proxy(Envoy / Linkerd)"]
AppB["App Container"]
end
AppA -- plaintext --> SidecarA
SidecarA -- mTLS --> SidecarB
SidecarB -- plaintext --> AppB
style SidecarA fill:#2d6a4f,color:#fff
style SidecarB fill:#2d6a4f,color:#fffThe app containers communicate in plaintext internally; the sidecar proxies handle all mTLS transparently.
Option 1: Service Mesh (Recommended)
Service meshes like Istio and Linkerd automatically inject a sidecar proxy into each pod and handle certificate issuance, rotation, and policy enforcement.
Istio
Strict mTLS (namespace-wide):
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: your-namespace
spec:
mtls:
mode: STRICTPermissive mode (useful during mesh migration β accepts both mTLS and plaintext):
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: your-namespace
spec:
mtls:
mode: PERMISSIVEDestinationRule β enforce mTLS on outbound traffic:
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: my-service-mtls
namespace: your-namespace
spec:
host: my-service
trafficPolicy:
tls:
mode: ISTIO_MUTUALISTIO_MUTUAL uses Istio-managed SPIFFE/SVID certificates. Use MUTUAL if you bring your own certificates.
Linkerd
Linkerd enables mTLS automatically for all meshed pods. Just annotate the deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: default
annotations:
linkerd.io/inject: enabled # Injects the Linkerd proxy sidecar
spec:
...Linkerd uses SPIFFE identities and rotates certificates automatically β zero cert management required.
Option 2: Manual mTLS with Kubernetes Secrets
For advanced use cases only Manual cert management is error-prone. Prefer a service mesh or cert-manager in production.
Step 1 β Generate certificates:
# CA
openssl genrsa -out ca.key 4096
openssl req -new -x509 -days 365 -key ca.key -out ca.crt -subj "/CN=my-ca"
# Server cert
openssl genrsa -out server.key 4096
openssl req -new -key server.key -out server.csr -subj "/CN=my-service"
openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt
# Client cert
openssl genrsa -out client.key 4096
openssl req -new -key client.key -out client.csr -subj "/CN=my-client"
openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crtStep 2 β Store as Kubernetes Secrets:
kubectl create secret generic server-tls \
--from-file=tls.crt=server.crt \
--from-file=tls.key=server.key \
--from-file=ca.crt=ca.crt
kubectl create secret generic client-tls \
--from-file=tls.crt=client.crt \
--from-file=tls.key=client.key \
--from-file=ca.crt=ca.crtStep 3 β Mount certs in your Pod:
apiVersion: v1
kind: Pod
metadata:
name: my-server
spec:
containers:
- name: app
image: my-app:latest
volumeMounts:
- name: tls-certs
mountPath: /etc/tls
readOnly: true
volumes:
- name: tls-certs
secret:
secretName: server-tlsOption 3: cert-manager
cert-manager automates certificate issuance and renewal inside Kubernetes.
# Self-signed ClusterIssuer
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: selfsigned-issuer
spec:
selfSigned: {}
---
# Certificate for a service
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: my-service-cert
namespace: default
spec:
secretName: my-service-tls
issuerRef:
name: selfsigned-issuer
kind: ClusterIssuer
dnsNames:
- my-service.default.svc.cluster.local
usages:
- client auth
- server authmTLS with Verified Identity
Plain mTLS and verified mTLS are subtly different things.
Plain mTLS (without SPIFFE)
Both sides present certificates and the TLS handshake succeeds if the certs are valid and signed by a trusted CA. But the server only knows the client has a valid cert β it doesnβt necessarily know which workload itβs talking to in a meaningful way.
Frontend β Backend
"Your cert is valid. Signed by our CA. OK, come in."
The identity in the certificate might just be a DNS name or a service account name β thereβs no guarantee it maps to a specific, attested workload.
mTLS with Verified Identity (with SPIFFE)
The certificate carries a SPIFFE ID in its SAN field β a URI like spiffe://example.org/ns/prod/sa/checkout. This ID was issued by SPIRE only after cryptographically attesting that the workload is genuinely running as that Kubernetes service account on a real node.
Frontend β Backend
"Your cert is valid, AND I can see your SPIFFE ID is
spiffe://example.org/ns/prod/sa/frontend.
I have a rule that says only that ID may call me. Allowed."
The difference is:
| Plain mTLS | mTLS + SPIFFE | |
|---|---|---|
| Connection encrypted | β | β |
| Cert cryptographically valid | β | β |
| Identity attested to a specific workload | β | β |
| Can authorize by workload identity | β | β |
| Cert rotation automatic | Depends | β |
| Works across clouds/clusters | Hard | β |
Why the distinction matters
Without SPIFFE, a stolen or misconfigured certificate from any service in your cluster might be accepted. With SPIFFE, the certificate is short-lived, tied to a specific attested workload, and your application can explicitly check "is the caller's SPIFFE ID one I expect?" β not just "is the cert valid?".
Itβs the difference between checking that someone has a valid government ID vs. checking that the ID belongs to the specific person you agreed to meet.
mTLS vs Other Auth Methods
| Feature | mTLS | JWT/OAuth | API Keys |
|---|---|---|---|
| Mutual Authentication | β | β one-way | β |
| Encrypted Transport | β | Depends on TLS | Depends on TLS |
| Certificate Rotation | Automated (mesh) | Token expiry | Manual |
| Zero Trust Ready | β | Partial | β |
| Best For | Service-to-service | User/app auth | Simple integrations |
Best Practices
Production Checklist
- Use a service mesh (Istio/Linkerd) β automates cert lifecycle and policy.
- Prefer STRICT mode over PERMISSIVE once all services are onboarded.
- Use SPIFFE/SPIRE for workload identity in multi-cluster or hybrid environments.
- Rotate certificates regularly β automate with cert-manager or your mesh.
- Monitor cert expiry β expired certs silently break mTLS with no helpful error.
- Combine with AuthorizationPolicy (Istio) to restrict which services can reach which.