Mutual TLS Design
Both sides prove their identity with certificates -- the server authenticates to the
client and the client authenticates to the server, establishing a cryptographically
verified service-to-service channel
When to Use
- Authenticating services to each other in a microservices architecture
- Implementing zero trust networking where network location is not sufficient for trust
- Replacing API keys or shared secrets for service-to-service communication
- Deploying a service mesh (Istio, Linkerd, Cilium) with automatic mTLS
- Building internal PKI for workload identity
- Meeting compliance requirements for encrypted and authenticated internal traffic
Threat Context
Standard TLS authenticates only the server -- the client verifies the server's
certificate, but the server has no cryptographic proof of the client's identity. In
service-to-service communication, this means any service on the network can call any other
service. API keys or bearer tokens add authentication but are static, shared, and
vulnerable to theft -- a stolen API key grants access until someone notices and rotates
it, which can be weeks or months. mTLS provides mutual cryptographic authentication: each
service has its own certificate issued by a trusted CA, and both sides verify the other's
identity during the TLS handshake. The 2017 Equifax breach exploited flat internal
networking with no service authentication -- internal services trusted any connection from
the internal network, allowing the attacker to move laterally through 48 databases after
exploiting a single Apache Struts vulnerability.
Instructions
Deploy an internal CA. Use Vault PKI, step-ca (Smallstep), cfssl, or a service
mesh's built-in CA. Do not use Let's Encrypt for internal mTLS -- Let's Encrypt
certificates are publicly trusted, intended for public-facing services, and require
public domain names. Internal CAs issue certificates trusted only within your
organization's trust store. This limits the blast radius of CA compromise and allows
you to issue certificates for internal service names that are not publicly resolvable.
Issue short-lived certificates. 24-72 hour certificate lifetimes eliminate the need
for revocation infrastructure. If a certificate is compromised, it expires within hours
rather than remaining valid for months. Automate certificate rotation with cert-manager
(Kubernetes), Vault Agent, or the service mesh's automatic certificate rotation. The
operational cost of short-lived certificates is higher, but the security benefit is
decisive: revocation (CRLs, OCSP) is complex, unreliable, and can be bypassed. Short
lifetimes make revocation unnecessary.
Use SPIFFE for workload identity. SPIFFE (Secure Production Identity Framework for
Everyone) standardizes workload identity as a URI:
spiffe://cluster.local/ns/production/sa/payment-service. SPIRE (the reference
implementation) acts as a workload attestation agent that verifies workload identity
using platform-specific selectors (Kubernetes service account, AWS instance metadata,
Docker container ID) and issues SVID (SPIFFE Verifiable Identity Document) certificates.
Service meshes (Istio, Linkerd) implement SPIFFE-compatible identity natively.
Configure certificate validation correctly. Both client and server must validate
the peer's certificate: verify the certificate chain (signed by a trusted CA), verify
the certificate is not expired, verify the SAN (Subject Alternative Name) matches the
expected service identity. Do not disable certificate verification in production --
ever. InsecureSkipVerify: true (Go), rejectUnauthorized: false (Node.js),
verify=False (Python requests) all defeat the entire purpose of mTLS by accepting
any certificate, including attacker-generated ones.
Enforce mTLS at the network level. In Kubernetes, use PeerAuthentication policies
(Istio) or NetworkPolicy with identity selectors (Cilium) to ensure that only mTLS
connections are accepted. Istio's STRICT mode rejects any plaintext connection to the
service. This prevents accidental fallback to unencrypted communication and ensures
that no service can communicate without presenting a valid identity certificate.
Handle the migration from plaintext to mTLS. In brownfield environments with
existing services communicating over plaintext, deploy mTLS in PERMISSIVE mode first
(accept both plaintext and mTLS). Monitor which services are still sending plaintext
using service mesh telemetry. Migrate them one by one, verifying each migration. Once
all services use mTLS, switch to STRICT mode. Istio's PeerAuthentication supports
per-namespace and per-service PERMISSIVE/STRICT configuration for gradual rollout.
Details
mTLS Handshake
The full TLS handshake with mutual authentication proceeds as follows:
- Client sends ClientHello (supported cipher suites, TLS version)
- Server responds with ServerHello, Server Certificate, and CertificateRequest
- Client verifies the server's certificate against its trust store
- Client sends its own Client Certificate and CertificateVerify message
- The CertificateVerify contains a signature over the handshake transcript, proving the
client possesses the private key corresponding to its certificate
- Server verifies the client's certificate against its trust store
- Both sides derive session keys and the encrypted channel is established
The critical difference from standard TLS is step 2 (CertificateRequest) and steps 4-6.
Without the CertificateRequest, the client never presents a certificate and the server
has no cryptographic proof of the client's identity.
Service Mesh mTLS
In Istio, Envoy sidecar proxies handle mTLS transparently. The application sends
plaintext to localhost on a designated port. The local Envoy sidecar intercepts the
outbound connection, establishes an mTLS connection to the destination's Envoy sidecar,
and forwards the traffic. The destination sidecar decrypts and delivers plaintext to the
destination application. No application code changes are required. Linkerd uses a similar
sidecar architecture with its own proxy (linkerd2-proxy, written in Rust). Cilium uses
eBPF to implement mTLS more efficiently without sidecars, reducing the per-pod resource
overhead.
SPIFFE/SPIRE Deep Dive
SPIRE has two components: the SPIRE Server (central authority that maintains the signing
CA, registration entries, and trust bundles) and the SPIRE Agent (runs on each node,
attests workloads, caches and distributes SVIDs). Workload attestation uses
platform-specific selectors: Kubernetes service account name and namespace, Docker
container labels, AWS instance ID and IAM role, bare-metal process UID and binary path.
This ensures that only the legitimate workload receives the certificate for its identity.
An attacker who compromises the host but not the workload's attestation properties cannot
obtain a valid SVID.
Debugging mTLS Failures
Common failure modes and their diagnostics:
| Symptom |
Likely Cause |
Diagnostic Command |
| Connection refused |
STRICT mode, client has no certificate |
Check PeerAuthentication policy |
| TLS handshake error |
Certificate not signed by trusted CA |
openssl s_client -cert ... -key ... |
| Identity mismatch |
SAN does not match expected identity |
openssl x509 -text -in cert.pem |
| Intermittent failures |
Certificate expired, clock skew |
Check system time, certificate validity |
| Works from one pod, not another |
Missing sidecar injection |
istioctl authn tls-check |
Anti-Patterns
InsecureSkipVerify: true in production. This disables certificate validation,
meaning any certificate (self-signed, expired, wrong identity) is accepted. This
provides encryption but zero authentication. A man-in-the-middle with any certificate
can intercept all traffic. This single line of code negates the entire mTLS deployment.
Long-lived client certificates. Certificates with 1-year lifetimes require
revocation infrastructure (CRLs, OCSP) that is complex and unreliable in practice. CRL
checking is best-effort in most TLS implementations, and OCSP has soft-fail behavior.
Use short-lived certificates (24-72 hours) with automatic rotation instead.
Sharing client certificates across services. If all services use the same client
certificate and private key, you cannot distinguish between services at the
authorization layer and cannot implement per-service access control. Each service must
have its own unique identity certificate. Sharing certificates also means that
compromising one service compromises the identity of all services.
mTLS without authorization. mTLS proves identity but does not enforce
authorization. After verifying the client's certificate identity (e.g.,
payment-service), you still need to check whether payment-service is authorized to
access the requested endpoint. Use service mesh authorization policies (Istio
AuthorizationPolicy, Cilium NetworkPolicy) to define which services can call which
endpoints.
Manual certificate distribution. Copying certificates to servers via SSH, baking
them into Docker images, or storing them in environment variables does not scale and
leads to expiration outages. Automate certificate lifecycle with SPIRE, cert-manager,
Vault Agent, or service mesh automatic rotation. If a human is involved in certificate
distribution, it will eventually fail.
1---2name: security-mtls-design3description: Mutual TLS Design4---5# Mutual TLS Design67> Both sides prove their identity with certificates -- the server authenticates to the8> client and the client authenticates to the server, establishing a cryptographically9> verified service-to-service channel1011## When to Use1213- Authenticating services to each other in a microservices architecture14- Implementing zero trust networking where network location is not sufficient for trust15- Replacing API keys or shared secrets for service-to-service communication16- Deploying a service mesh (Istio, Linkerd, Cilium) with automatic mTLS17- Building internal PKI for workload identity18- Meeting compliance requirements for encrypted and authenticated internal traffic1920## Threat Context2122Standard TLS authenticates only the server -- the client verifies the server's23certificate, but the server has no cryptographic proof of the client's identity. In24service-to-service communication, this means any service on the network can call any other25service. API keys or bearer tokens add authentication but are static, shared, and26vulnerable to theft -- a stolen API key grants access until someone notices and rotates27it, which can be weeks or months. mTLS provides mutual cryptographic authentication: each28service has its own certificate issued by a trusted CA, and both sides verify the other's29identity during the TLS handshake. The 2017 Equifax breach exploited flat internal30networking with no service authentication -- internal services trusted any connection from31the internal network, allowing the attacker to move laterally through 48 databases after32exploiting a single Apache Struts vulnerability.3334## Instructions35361. **Deploy an internal CA.** Use Vault PKI, step-ca (Smallstep), cfssl, or a service37 mesh's built-in CA. Do not use Let's Encrypt for internal mTLS -- Let's Encrypt38 certificates are publicly trusted, intended for public-facing services, and require39 public domain names. Internal CAs issue certificates trusted only within your40 organization's trust store. This limits the blast radius of CA compromise and allows41 you to issue certificates for internal service names that are not publicly resolvable.42432. **Issue short-lived certificates.** 24-72 hour certificate lifetimes eliminate the need44 for revocation infrastructure. If a certificate is compromised, it expires within hours45 rather than remaining valid for months. Automate certificate rotation with cert-manager46 (Kubernetes), Vault Agent, or the service mesh's automatic certificate rotation. The47 operational cost of short-lived certificates is higher, but the security benefit is48 decisive: revocation (CRLs, OCSP) is complex, unreliable, and can be bypassed. Short49 lifetimes make revocation unnecessary.50513. **Use SPIFFE for workload identity.** SPIFFE (Secure Production Identity Framework for52 Everyone) standardizes workload identity as a URI:53 `spiffe://cluster.local/ns/production/sa/payment-service`. SPIRE (the reference54 implementation) acts as a workload attestation agent that verifies workload identity55 using platform-specific selectors (Kubernetes service account, AWS instance metadata,56 Docker container ID) and issues SVID (SPIFFE Verifiable Identity Document) certificates.57 Service meshes (Istio, Linkerd) implement SPIFFE-compatible identity natively.58594. **Configure certificate validation correctly.** Both client and server must validate60 the peer's certificate: verify the certificate chain (signed by a trusted CA), verify61 the certificate is not expired, verify the SAN (Subject Alternative Name) matches the62 expected service identity. Do not disable certificate verification in production --63 ever. `InsecureSkipVerify: true` (Go), `rejectUnauthorized: false` (Node.js),64 `verify=False` (Python requests) all defeat the entire purpose of mTLS by accepting65 any certificate, including attacker-generated ones.66675. **Enforce mTLS at the network level.** In Kubernetes, use PeerAuthentication policies68 (Istio) or NetworkPolicy with identity selectors (Cilium) to ensure that only mTLS69 connections are accepted. Istio's STRICT mode rejects any plaintext connection to the70 service. This prevents accidental fallback to unencrypted communication and ensures71 that no service can communicate without presenting a valid identity certificate.72736. **Handle the migration from plaintext to mTLS.** In brownfield environments with74 existing services communicating over plaintext, deploy mTLS in PERMISSIVE mode first75 (accept both plaintext and mTLS). Monitor which services are still sending plaintext76 using service mesh telemetry. Migrate them one by one, verifying each migration. Once77 all services use mTLS, switch to STRICT mode. Istio's PeerAuthentication supports78 per-namespace and per-service PERMISSIVE/STRICT configuration for gradual rollout.7980## Details8182### mTLS Handshake8384The full TLS handshake with mutual authentication proceeds as follows:85861. Client sends ClientHello (supported cipher suites, TLS version)872. Server responds with ServerHello, Server Certificate, and CertificateRequest883. Client verifies the server's certificate against its trust store894. Client sends its own Client Certificate and CertificateVerify message905. The CertificateVerify contains a signature over the handshake transcript, proving the91 client possesses the private key corresponding to its certificate926. Server verifies the client's certificate against its trust store937. Both sides derive session keys and the encrypted channel is established9495The critical difference from standard TLS is step 2 (CertificateRequest) and steps 4-6.96Without the CertificateRequest, the client never presents a certificate and the server97has no cryptographic proof of the client's identity.9899### Service Mesh mTLS100101In Istio, Envoy sidecar proxies handle mTLS transparently. The application sends102plaintext to localhost on a designated port. The local Envoy sidecar intercepts the103outbound connection, establishes an mTLS connection to the destination's Envoy sidecar,104and forwards the traffic. The destination sidecar decrypts and delivers plaintext to the105destination application. No application code changes are required. Linkerd uses a similar106sidecar architecture with its own proxy (linkerd2-proxy, written in Rust). Cilium uses107eBPF to implement mTLS more efficiently without sidecars, reducing the per-pod resource108overhead.109110### SPIFFE/SPIRE Deep Dive111112SPIRE has two components: the SPIRE Server (central authority that maintains the signing113CA, registration entries, and trust bundles) and the SPIRE Agent (runs on each node,114attests workloads, caches and distributes SVIDs). Workload attestation uses115platform-specific selectors: Kubernetes service account name and namespace, Docker116container labels, AWS instance ID and IAM role, bare-metal process UID and binary path.117This ensures that only the legitimate workload receives the certificate for its identity.118An attacker who compromises the host but not the workload's attestation properties cannot119obtain a valid SVID.120121### Debugging mTLS Failures122123Common failure modes and their diagnostics:124125| Symptom | Likely Cause | Diagnostic Command |126| ------------------------------- | -------------------------------------- | --------------------------------------- |127| Connection refused | STRICT mode, client has no certificate | Check PeerAuthentication policy |128| TLS handshake error | Certificate not signed by trusted CA | `openssl s_client -cert ... -key ...` |129| Identity mismatch | SAN does not match expected identity | `openssl x509 -text -in cert.pem` |130| Intermittent failures | Certificate expired, clock skew | Check system time, certificate validity |131| Works from one pod, not another | Missing sidecar injection | `istioctl authn tls-check` |132133## Anti-Patterns1341351. **`InsecureSkipVerify: true` in production.** This disables certificate validation,136 meaning any certificate (self-signed, expired, wrong identity) is accepted. This137 provides encryption but zero authentication. A man-in-the-middle with any certificate138 can intercept all traffic. This single line of code negates the entire mTLS deployment.1391402. **Long-lived client certificates.** Certificates with 1-year lifetimes require141 revocation infrastructure (CRLs, OCSP) that is complex and unreliable in practice. CRL142 checking is best-effort in most TLS implementations, and OCSP has soft-fail behavior.143 Use short-lived certificates (24-72 hours) with automatic rotation instead.1441453. **Sharing client certificates across services.** If all services use the same client146 certificate and private key, you cannot distinguish between services at the147 authorization layer and cannot implement per-service access control. Each service must148 have its own unique identity certificate. Sharing certificates also means that149 compromising one service compromises the identity of all services.1501514. **mTLS without authorization.** mTLS proves identity but does not enforce152 authorization. After verifying the client's certificate identity (e.g.,153 `payment-service`), you still need to check whether `payment-service` is authorized to154 access the requested endpoint. Use service mesh authorization policies (Istio155 AuthorizationPolicy, Cilium NetworkPolicy) to define which services can call which156 endpoints.1571585. **Manual certificate distribution.** Copying certificates to servers via SSH, baking159 them into Docker images, or storing them in environment variables does not scale and160 leads to expiration outages. Automate certificate lifecycle with SPIRE, cert-manager,161 Vault Agent, or service mesh automatic rotation. If a human is involved in certificate162 distribution, it will eventually fail.