What Is Mutual TLS?
In standard TLS, only the server presents a certificate to prove its identity to the client. In Mutual TLS (mTLS), both sides present certificates — the client proves its identity to the server as well.
mTLS is the gold standard for service-to-service authentication in zero-trust architectures. It is used by Kubernetes service meshes (Istio, Linkerd), financial APIs, and anywhere cryptographic client identity assurance is required.
One-Way vs Mutual TLS
| One-Way TLS | Mutual TLS | |
|---|---|---|
| Server authenticates | Yes | Yes |
| Client authenticates | No | Yes |
| Client needs certificate | No | Yes |
| Use case | HTTPS websites | Service-to-service APIs |
Certificate Authority Setup
For mTLS, you typically run your own private CA to issue client certificates. Use a CA trusted by your servers but not public CAs:
# Generate a private CA key and self-signed certificate
openssl genrsa -out ca.key 4096
openssl req -new -x509 -days 3650 -key ca.key \
-subj '/CN=My Internal CA/O=Example Corp' \
-out ca.crt
Client Certificate Generation
Each client service gets its own key pair and certificate signed by the CA:
# 1. Generate client key
openssl genrsa -out client.key 2048
# 2. Create a Certificate Signing Request (CSR)
openssl req -new -key client.key \
-subj '/CN=payment-service/O=Example Corp' \
-out client.csr
# 3. Sign with the CA
openssl x509 -req -days 365 -in client.csr \
-CA ca.crt -CAkey ca.key -CAcreateserial \
-out client.crt
The CN (Common Name) becomes the client's identity on the server side — use it to implement fine-grained authorization (payment-service may call /payments/* but not /admin/*).
Server Configuration
Nginx:
server {
listen 443 ssl;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.key;
# mTLS: require client certificate signed by our CA
ssl_client_certificate /etc/ssl/ca.crt;
ssl_verify_client on; # require valid client cert
ssl_verify_depth 2;
# Pass client CN to application
proxy_set_header X-Client-Cert-CN $ssl_client_s_dn_cn;
}
Testing with curl
# Test without client certificate (should fail with 400)
curl https://api.example.com/secure
# Test with client certificate
curl --cert client.crt --key client.key \
--cacert ca.crt \
https://api.example.com/secure
# Inspect server certificate chain
openssl s_client -connect api.example.com:443 \
-cert client.crt -key client.key -CAfile ca.crt
mTLS in Kubernetes
Service meshes automate mTLS certificate management:
- Istio: enables mTLS between all pods in a mesh with a
PeerAuthenticationpolicy. Certificates are rotated automatically every 24 hours using SPIFFE/SPIRE. - Linkerd: strict mTLS by default with automatic certificate rotation from a trust anchor.
# Istio: enforce strict mTLS in a namespace
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: production
spec:
mtls:
mode: STRICT
Common Errors
| Error | Cause | Fix |
|---|---|---|
| `400 No required SSL certificate was sent` | Missing client cert | Pass `--cert` to curl |
| `SSL certificate verify failed` | CA mismatch | Use the same CA for client cert signing |
| `Certificate has expired` | Cert past `notAfter` | Renew and reissue |
| `ssl_verify_client` returns 400 | Wrong CA configured | Check `ssl_client_certificate` path |