Skip to content

mTLS (Mutual TLS)

  • Standard TLS authenticates the server to the client only (one-way)
  • mTLS authenticates both sides in the same handshake (two-way)
  • The TLS handshake itself is the authentication — no passwords, tokens, or API keys
  • Same X.509 certs + CA trust model as regular TLS (see 02.certificates)

TLS vs mTLS

Step TLS mTLS
Server presents certificate yes yes
Client verifies server cert against trusted CAs yes yes
Client presents certificate no yes
Server verifies client cert against trusted CAs no yes
  • In plain TLS, the server has no idea who the client is at the TLS layer — auth happens later (cookie, JWT, API key)
  • In mTLS, the server already knows the client's identity before the first HTTP byte is sent

Handshake (what changes from regular TLS 1.3)

Compared to the flow in 03.https, only two extra messages make it mTLS:

  1. ClientHello — same as regular TLS
  2. ServerHello — same as regular TLS
  3. Server handshake messages (encrypted) — Server adds a CertificateRequest here, telling the client "I want your cert too". This is the trigger for mTLS.
  4. Client handshake messages (encrypted) — Client now responds with:
    • Certificate — its X.509 chain
    • CertificateVerify — a signature over the handshake transcript using the client's private key, proving it actually owns the cert it just presented
    • Finished — handshake MAC
  5. Server verifies the client:
    • Cert chain is signed by a CA the server trusts
    • The CertificateVerify signature is valid for the public key in the cert
    • Cert hasn't expired, isn't revoked, identity matches policy
  6. Application data — same as regular TLS, encrypted with derived symmetric session keys

  7. A stolen cert alone is useless: without the matching private key, CertificateVerify can't be produced

  8. mTLS uses the same ECDHE key exchange as TLS 1.3 — forward secrecy still applies

Trust model

  • Both sides need a CA they trust
  • Server trusts a CA → accepts any client cert signed by it
  • Client trusts a CA → accepts any server cert signed by it
  • For server auth on the public web, the CA is usually a public one (Let's Encrypt, DigiCert) installed in the OS trust store
  • For client auth in mTLS, the CA is almost always private — only people the CA owner explicitly signs can connect

Issuing client certs

  • Same flow as server certs: Certificate Sign Request → CA signs → cert returned (see 02.certificates)
  • The private key never leaves the client
# Generate keypair (ECDSA is preferred over RSA for new deployments)
openssl ecparam -name prime256v1 -genkey -noout -out client.key

# Build CSR — public key + claimed identity
openssl req -new -key client.key -out client.csr \
  -subj "/CN=my-service" \
  -addext "subjectAltName=DNS:my-service.example.com"

# Inspect what's in the CSR
openssl req -in client.csr -noout -text

# Send client.csr to the CA. NEVER send client.key.
# CA signs and returns client.crt.

Identity in the cert

  • The server pulls the client's identity out of the cert during the handshake
  • Modern verifiers read the SAN (Subject Alternative Name), not the CN
  • SAN entries can be DNS, IP, URI, or email
  • SPIFFE URIs (spiffe://trust-domain/path) are increasingly common for service-to-service auth — used in Istio, Linkerd, and SaaS connectors. Lets the server route by tenant/service identity without DNS or IP lookups.

Using the cert and key

# curl with client cert
curl --cert client.crt --key client.key https://api.example.com/

# Test an mTLS endpoint
openssl s_client -connect api.example.com:443 \
  -cert client.crt -key client.key \
  -CAfile ca.crt
  • In Kubernetes, cert + key are typically mounted from a Secret as files or env vars
  • Sidecar proxies (Envoy, Istio, Linkerd) often terminate mTLS so apps don't have to deal with certs at all

When to use mTLS

  • Service-to-service auth in a zero-trust network
  • B2B integrations where the vendor needs strong identity (payment processors, SaaS connectors)
  • IoT / fleet devices that can't manage passwords or tokens
  • Outbound-only tunnels where the vendor must verify who is connecting (the connection is initiated from the client side, so a static API key would be the only alternative)

When it's overkill

  • User-facing web apps — users won't manage certs; use OAuth/SSO instead
  • Public APIs with thousands of unknown callers — use API keys + rate limiting
  • Anywhere a stolen bearer token is an acceptable failure mode

Operational notes

  • Certs expire — set up renewal (cert-manager, ACME, vendor portal) before they do
  • Revocation is hard: CRLs and OCSP exist but are spotty in practice; short cert lifetimes (hours/days) are the modern answer
  • Private keys must be stored encrypted at rest (KMS, secret manager) and never logged
  • Rotate the keypair periodically, not just the cert — re-signing the same public key forever defeats the point
  • Watch for clock skew: a cert that's "not yet valid" by 30 seconds will fail the handshake just like an expired one