kubernetes/networking

TLS
mTLS in Kubernetes - A Hands-On Project

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:#fff

The app containers communicate in plaintext internally; the sidecar proxies handle all mTLS transparently.

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: STRICT

Permissive 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: PERMISSIVE

DestinationRule β€” 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_MUTUAL

ISTIO_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.crt

Step 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.crt

Step 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-tls

Option 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 auth

mTLS with Verified Identity

SPIFFE

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 mTLSmTLS + SPIFFE
Connection encryptedβœ…βœ…
Cert cryptographically validβœ…βœ…
Identity attested to a specific workloadβŒβœ…
Can authorize by workload identityβŒβœ…
Cert rotation automaticDependsβœ…
Works across clouds/clustersHardβœ…

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

FeaturemTLSJWT/OAuthAPI Keys
Mutual Authenticationβœ…βŒ one-way❌
Encrypted Transportβœ…Depends on TLSDepends on TLS
Certificate RotationAutomated (mesh)Token expiryManual
Zero Trust Readyβœ…Partial❌
Best ForService-to-serviceUser/app authSimple 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.

Further Reading