PKI & TLS

Mutual TLS

Client certificates and service-to-service authentication.

In ordinary TLS only the server proves who it is; the client stays anonymous and authenticates later with a password or token. Mutual TLS (mTLS) makes both ends present certificates, so the server cryptographically knows which client it is talking to before any application data flows. This is the backbone of zero-trust networking and service meshes, and it is far easier to reason about once you have run it locally.

Why This Matters

Inside a cluster, “the request came from inside the network” is no longer a security boundary. mTLS replaces implicit network trust with per-connection cryptographic identity: a service only accepts calls from peers holding a certificate issued by a CA it trusts. This is how Istio, Linkerd, and SPIFFE authenticate workloads, and it directly connects to the Kubernetes Security foundation.

Server-Only vs Mutual Authentication

AspectServer-only TLSMutual TLS
Who presents a certificateServerBoth server and client
Client identityUnknown at TLS layerCryptographically verified
Extra handshake messageNoneCertificateRequest from server
Typical usePublic websitesService-to-service, APIs, zero trust
Client auth done byPassword, token, cookieThe certificate itself

The only handshake difference is that the server sends a CertificateRequest, and the client responds with its Certificate and a CertificateVerify signature proving it holds the matching private key. Everything else matches the sibling TLS Handshake page.

Hands-On mTLS Between Two Local Services

Reuse the lab CA from the sibling Certificate Authorities page to issue both a server and a client certificate, then require the client cert.

cd ~/crypto-lab
# Issue server and client certs from the lab CA
step certificate create server.local server.crt server.key \
  --ca ~/.step/certs/intermediate_ca.crt --ca-key ~/.step/secrets/intermediate_ca_key \
  --no-password --insecure
step certificate create client.local client.crt client.key \
  --ca ~/.step/certs/intermediate_ca.crt --ca-key ~/.step/secrets/intermediate_ca_key \
  --no-password --insecure

# Start a server that REQUIRES a client cert signed by our CA
openssl s_server -accept 8443 -cert server.crt -key server.key \
  -CAfile ~/.step/certs/root_ca.crt -Verify 1 -www &

Now connect first without a client certificate, then with one.

# No client cert: rejected
openssl s_client -connect 127.0.0.1:8443 -CAfile ~/.step/certs/root_ca.crt </dev/null 2>&1 \
  | grep -E "verify error|alert"
# With client cert: accepted
openssl s_client -connect 127.0.0.1:8443 -CAfile ~/.step/certs/root_ca.crt \
  -cert client.crt -key client.key </dev/null 2>&1 | grep "Verify return code"
kill %1

Expected output:

140... :error:...:SSL alert number 40
    Verify return code: 0 (ok)

The first attempt is refused because the server demanded a certificate and got none; the second succeeds because the client presented a certificate chaining to the CA the server trusts.

Where mTLS Shows Up

  • Service meshes (Istio, Linkerd) issue short-lived certificates to every pod and enforce mTLS transparently between them.
  • Kubernetes uses mTLS internally: the kubelet, API server, and etcd all authenticate each other with certificates.
  • SPIFFE/SPIRE gives each workload a cryptographic identity (a SPIFFE ID) embedded in a certificate.
  • Zero-trust architectures use mTLS so that every connection is authenticated regardless of network location.

The operational catch is certificate lifecycle: with thousands of workloads, certificates must be issued, rotated, and revoked automatically. This is why meshes bundle their own CA and rotate certificates on the order of hours.

Practical Guidance

  1. Scope the trust store tightly: a server doing mTLS should trust only the specific CA that issues client certificates, not the public root store.
  2. Automate issuance and rotation; manual client-certificate management does not scale past a handful of services.
  3. Use short-lived client certificates so a leaked key expires quickly, removing the need for reliable revocation.
  4. Bind authorization to certificate identity (SAN or SPIFFE ID), not just “presented any valid cert”; authentication is not authorization.
  5. Adopt a mesh or step ca for real deployments rather than hand-managing keys; the plumbing is where mTLS projects fail.