What Is Mutual TLS?
Standard TLS is one-sided: the client verifies the server's certificate, but the server accepts any client. Mutual TLS (mTLS) adds a second check — the server also demands a certificate from the client and validates it against a trusted CA. Both sides prove their identity before any application data flows.
In a typical HTTPS connection:
- Client connects to server
- Server presents its certificate
- Client verifies the certificate against its trust store
- Encrypted channel established
With mTLS, two additional steps happen:
- Client connects to server
- Server presents its certificate
- Client verifies the server certificate
- Server requests the client's certificate
- Client presents its certificate, server verifies it against a trusted CA
- Encrypted channel established with both parties authenticated
Why mTLS Matters
Zero Trust Architecture
In zero trust, network location grants no implicit trust. A request from inside your VPC is treated the same as one from the public internet. mTLS enforces identity at the transport layer — every connection must present a valid, CA-signed certificate. No certificate, no connection.
Service Mesh Communication
Service meshes like Istio, Linkerd, and Consul Connect use mTLS by default for pod-to-pod traffic. Every sidecar proxy holds a short-lived certificate issued by the mesh's internal CA. This encrypts all east-west traffic and provides cryptographic identity for authorization policies.
API Authentication
Client certificates are an alternative to API keys or OAuth tokens for machine-to-machine communication. They are harder to steal (bound to a private key that never leaves the client), cannot be accidentally logged, and are validated at the TLS layer before your application code runs.
When to Use mTLS
- Service-to-service communication in microservices
- B2B API integrations where both parties need strong identity
- IoT device authentication
- Internal admin interfaces and dashboards
- Database connections (PostgreSQL, MySQL both support client certs)
Step-by-Step: Setting Up mTLS
Step 1: Generate the CA Certificate
You need a CA that both the server and clients trust. You can generate one at getaCert.com using the CA option, or with openssl:
# Generate CA private key
openssl genrsa -out ca.key 4096
# Generate CA certificate (valid for 10 years)
openssl req -new -x509 -key ca.key -sha256 \
-subj "/CN=My Internal CA/O=My Company" \
-days 3650 -out ca.crt
Guard ca.key carefully. Anyone with this key can issue trusted client certificates.
Step 2: Generate the Server Certificate
The server certificate is a standard TLS cert. Generate one at getaCert.com or with openssl:
# Generate server key and CSR
openssl genrsa -out server.key 2048
openssl req -new -key server.key \
-subj "/CN=api.example.com" -out server.csr
# Sign with the CA
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -days 365 -sha256 \
-extfile <(echo "subjectAltName=DNS:api.example.com") \
-out server.crt
Step 3: Generate a Client Certificate
Generate a CA-signed client certificate at getaCert.com, or create one manually:
# Generate client key and CSR
openssl genrsa -out client.key 2048
openssl req -new -key client.key \
-subj "/CN=service-a/O=My Company" -out client.csr
# Sign with the same CA
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -days 365 -sha256 -out client.crt
The CN (Common Name) identifies the client. Your server can extract it from the validated certificate and use it for authorization.
Step 4: Verify the Certificates
Before configuring any server, confirm the chain is valid:
# Verify server cert against CA
openssl verify -CAfile ca.crt server.crt
# Verify client cert against CA
openssl verify -CAfile ca.crt client.crt
# Inspect client cert details
openssl x509 -in client.crt -text -noout
Both commands should print OK.
Configuring nginx for mTLS
Basic mTLS Configuration
server {
listen 443 ssl;
server_name api.example.com;
# Server certificate (standard TLS)
ssl_certificate /etc/nginx/ssl/server.crt;
ssl_certificate_key /etc/nginx/ssl/server.key;
# Client certificate verification (mTLS)
ssl_client_certificate /etc/nginx/ssl/ca.crt;
ssl_verify_client on;
# TLS settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
# Pass client identity to upstream
proxy_set_header X-Client-CN $ssl_client_s_dn_cn;
proxy_set_header X-Client-Verify $ssl_client_verify;
proxy_pass http://backend:8080;
}
}
Key directives:
| Directive | Purpose |
|---|---|
ssl_client_certificate |
CA cert used to verify client certificates |
ssl_verify_client on |
Require a valid client cert (connection refused without one) |
ssl_verify_client optional |
Request a client cert but allow connections without one |
ssl_verify_depth |
Max depth of the certificate chain (default: 1) |
$ssl_client_s_dn_cn |
Extracts the client certificate's Common Name |
$ssl_client_verify |
Result of verification: SUCCESS, FAILED, or NONE |
Optional Client Verification
For endpoints where some clients use certificates and others use API keys:
ssl_verify_client optional;
location /api/ {
if ($ssl_client_verify != SUCCESS) {
return 403;
}
proxy_pass http://backend:8080;
}
location /public/ {
proxy_pass http://backend:8080;
}
Configuring Envoy for mTLS
Envoy is the data plane proxy behind Istio and many service meshes. Here is a listener configuration for mTLS:
static_resources:
listeners:
- name: mtls_listener
address:
socket_address:
address: 0.0.0.0
port_value: 8443
filter_chains:
- transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
require_client_certificate: true
common_tls_context:
tls_certificates:
- certificate_chain:
filename: /etc/envoy/ssl/server.crt
private_key:
filename: /etc/envoy/ssl/server.key
validation_context:
trusted_ca:
filename: /etc/envoy/ssl/ca.crt
filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
route_config:
virtual_hosts:
- name: backend
domains: ["*"]
routes:
- match: { prefix: "/" }
route: { cluster: backend_service }
The critical field is require_client_certificate: true. Without it, Envoy accepts connections without a client cert.
Testing mTLS with curl
Successful Connection
curl --cert client.crt --key client.key --cacert ca.crt \
https://api.example.com/health
All three flags are needed:
--cert— the client certificate to present--key— the client's private key--cacert— the CA to verify the server's certificate
Expected Failures
Without a client certificate:
curl --cacert ca.crt https://api.example.com/health
# curl: (56) OpenSSL SSL3 read bytes: sslv3 alert handshake failure
With an untrusted client certificate:
curl --cert wrong-client.crt --key wrong-client.key --cacert ca.crt \
https://api.example.com/health
# curl: (56) OpenSSL SSL3 read bytes: tlsv1 alert certificate unknown
Both fail at the TLS layer. No HTTP request is ever sent.
Certificate Revocation
When a client certificate is compromised, you need to revoke it. Two mechanisms:
CRL (Certificate Revocation List)
Generate a CRL and configure nginx to check it:
openssl ca -gencrl -out crl.pem -config ca.cnf
ssl_crl /etc/nginx/ssl/crl.pem;
OCSP Stapling
For larger deployments, OCSP is more efficient than distributing CRL files. step-ca supports OCSP out of the box.
Common Pitfalls
Certificate chain issues. If the client cert is signed by an intermediate CA, the ssl_client_certificate file must contain the full chain (intermediate + root).
Clock skew. Certificates have notBefore and notAfter timestamps. Containers with incorrect system clocks will reject valid certificates. Use NTP.
Key format mismatch. Ensure keys are in PEM format. DER-encoded keys will cause silent failures.
Forgetting SAN for servers. Modern TLS libraries ignore the CN field for server identity. Always set Subject Alternative Names on server certificates.
Summary
mTLS provides cryptographic identity for both client and server. It is the foundation of zero-trust networking and is built into every major service mesh. The setup involves three certificates — CA, server, and client — and a server configured to verify clients against the CA.
For quick mTLS testing, generate a CA-signed client certificate at getaCert.com and test it against our mTLS test endpoint at getacert.com:8443. No signup, no waiting.