What is mTLS (Mutual TLS)
Key Takeaways
- mTLS adds client certificate authentication to the standard TLS handshake — both sides prove identity cryptographically
- Primary use case: service-to-service authentication in zero-trust architectures where network location isn't trusted
- Service meshes (Istio, Linkerd) automate mTLS between pods — no application code changes required
- Most mTLS failures come from certificate chain mismatches, expired client certs, or TLS termination stripping the client certificate before it reaches the backend
Mutual TLS (mTLS) is TLS with bidirectional certificate authentication. In standard TLS, only the server presents a certificate — the client verifies the server’s identity but remains anonymous at the transport layer. In mTLS, the server also requests a certificate from the client during the handshake. Both sides verify each other’s certificate chain before the connection is established. If either certificate is invalid, expired, or untrusted, the handshake fails.
Why it matters
- Service-to-service authentication — in microservice architectures, services need to verify they’re talking to legitimate peers, not compromised or rogue services. mTLS provides cryptographic proof of identity without relying on network segmentation or shared secrets.
- Zero-trust enforcement — zero-trust assumes the network is hostile. mTLS ensures that even if an attacker gains network access, they can’t communicate with services without a valid client certificate issued by the trusted CA.
- Stronger than API keys — API keys are bearer tokens: anyone who has the key can use it. Client certificates are bound to a private key that never leaves the service. Stealing the certificate without the private key is useless.
- Eliminates credential rotation pain — unlike passwords or API keys that must be rotated and distributed, certificate-based identity integrates with automated issuance (ACME, cert-manager). Rotation happens via standard certificate renewal.
- Audit trail — the client certificate’s subject (CN, SAN) identifies which service made the request. This is logged at the TLS layer, providing attribution without application-level auth headers.
How it works
- Client sends ClientHello — standard TLS handshake begins
- Server responds with ServerHello + Certificate — server presents its certificate as normal
- Server sends CertificateRequest — server requests a client certificate, specifying acceptable CA names and signature algorithms
- Client sends its Certificate — client presents its own certificate (end entity + chain)
- Client sends CertificateVerify — client signs the handshake transcript with its private key, proving it owns the certificate
- Server validates client certificate — checks the chain against its trusted CA bundle, verifies the signature, checks expiry and revocation
- Handshake completes — both sides are authenticated. Application data flows encrypted.
If the client doesn’t have a certificate (or sends an empty certificate message), the server can either reject the connection or fall back to one-way TLS, depending on configuration (ssl_verify_client optional vs required in Nginx).
In real systems
Nginx — requiring client certificates:
server {
listen 443 ssl;
ssl_certificate /etc/ssl/server.pem;
ssl_certificate_key /etc/ssl/server-key.pem;
# mTLS configuration
ssl_client_certificate /etc/ssl/trusted-client-ca.pem; # CA that signed client certs
ssl_verify_client required; # Reject connections without valid client cert
ssl_verify_depth 2; # Allow intermediate CAs in client chain
# Pass client identity to backend
proxy_set_header X-Client-CN $ssl_client_s_dn_cn;
proxy_set_header X-Client-Cert $ssl_client_escaped_cert;
}
Istio service mesh — automatic mTLS:
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: production
spec:
mtls:
mode: STRICT # All traffic must be mTLS
Istio’s sidecar proxy (Envoy) handles certificate issuance, rotation, and mTLS negotiation transparently. Application code never touches certificates.
curl with client certificate:
curl --cert client.pem --key client-key.pem --cacert server-ca.pem \
https://api.internal.example.com/v1/data
Kubernetes API server — uses mTLS for all communication. Kubelets, controllers, and kubectl all authenticate with client certificates. The certificates are issued by the cluster’s internal CA and include group information in the Organization field for RBAC.
Where it breaks
TLS termination strips client certificate — a load balancer terminates TLS and forwards plaintext HTTP to the backend. The client certificate was verified at the LB, but the backend never sees it. The LB must forward the client certificate (or its DN) via headers like X-Client-Cert or X-Forwarded-Client-Cert. If this header isn’t configured, the backend has no way to identify the client. Worse: if the backend trusts this header without verifying it came from the LB, any client can spoof it by setting the header directly.
Client certificate CA mismatch — the server’s ssl_client_certificate points to CA-A, but the client’s certificate was signed by CA-B. The handshake fails with certificate unknown or unknown CA. In environments with multiple internal CAs (common after mergers or in multi-team orgs), the server must trust all relevant CAs. Missing one CA in the bundle silently blocks that team’s services.
Expired client certificates in automated systems — a service’s client certificate expires, but unlike browser-facing certificates (where users see a warning), service-to-service mTLS failures are silent from the user’s perspective. The service simply can’t reach its dependencies. Logs show TLS handshake failures, but without certificate monitoring covering client certs (not just server certs), the root cause takes time to identify.
Operational insight
The hardest part of mTLS at scale isn’t the cryptography — it’s certificate identity mapping. Once you have mTLS, you need to answer: “This client presented certificate with CN=payment-service. What is it allowed to do?” This requires mapping certificate identities to authorization policies. In Kubernetes, SPIFFE IDs (spiffe://cluster.local/ns/production/sa/payment-service) solve this by encoding namespace and service account into the certificate’s SAN. Without a structured identity scheme, you end up with ad-hoc CN-based allow lists that become unmaintainable as services multiply.
Related topics
Ready to Secure Your Enterprise?
Experience how our cryptographic solutions simplify, centralize, and automate identity management for your entire organization.