Protocol

Mutual TLS (mTLS): client certificates and when to use them

By · Last verified

Mutual TLS (mTLS) is TLS in which both ends present certificates: the server proves it is api.example.com, and the client proves it is, say, billing-service. It is the standard way to authenticate machines to machines because the credential is a private key that never leaves the client. Use a private CA; public certificates are being limited to server authentication.

How mTLS changes the TLS 1.3 handshake

mTLS adds one server message and two client messages to an otherwise normal TLS 1.3 handshake. TLS 1.3 was originally specified in RFC 8446; RFC 9846 (July 2026) now obsoletes it but keeps the same message flow.

Client                                                 Server
------                                                 ------
ClientHello            (key_share, signature_algorithms) -->
                                              <-- ServerHello (key_share)
                                              <-- {EncryptedExtensions}
                                              <-- {CertificateRequest}     <- mTLS
                                              <-- {Certificate}            server chain
                                              <-- {CertificateVerify}      server proves key
                                              <-- {Finished}
{Certificate}          client chain                        -->             <- mTLS
{CertificateVerify}    signature over transcript           -->             <- mTLS
{Finished}                                                 -->
[Application Data]                                     <-->  [Application Data]

{ } = encrypted with handshake keys      [ ] = encrypted with application keys

What each mTLS message carries:

Message Sent by Contents Purpose
CertificateRequest Server signature_algorithms (required), optionally certificate_authorities with the distinguished names of acceptable CAs Asks the client for a certificate and says which CAs and algorithms are acceptable
Certificate Client The client's leaf certificate and any intermediates; may be empty if it has none Presents the identity
CertificateVerify Client A signature over the handshake transcript with the client's private key Proves possession of the key, binding it to this specific connection

Three details matter in practice:

  • The client certificate is encrypted in TLS 1.3. In TLS 1.2 it was sent before encryption started, so a passive observer could read the client's identity. TLS 1.3 sends it under handshake keys.
  • An empty Certificate is allowed by the protocol. The server decides what to do. A strict server aborts with the certificate_required alert (alert number 116); a lenient one continues and lets the application decide.
  • Post-handshake authentication exists but is limited. A TLS 1.3 client that sends the post_handshake_auth extension lets the server request a certificate later in the connection. RFC 8740 forbids this on HTTP/2, which is why "require a client certificate only on /admin" usually does not work with HTTP/2 and needs a separate hostname instead.

One-way TLS versus mutual TLS

One-way TLS authenticates the server; mTLS authenticates both sides with the same mechanism. The table compares them with the usual alternative, bearer tokens over one-way TLS.

One-way TLS + API key or token Mutual TLS
Server authenticated Yes, by certificate Yes, by certificate
Client authenticated By a secret sent inside the connection By a certificate and a private-key signature in the handshake
What leaks if traffic is logged or a proxy is compromised The token itself, reusable until revoked Nothing reusable; the private key never leaves the client
Rejection point Application layer, after TLS TLS layer, before any HTTP request is parsed
Rotation Replace the secret everywhere Issue a new certificate; CA trust stays the same
Operational cost Low Needs a CA, issuance, renewal and revocation

mTLS is not a replacement for authorization. It tells you which client is connecting; your application still decides what that client may do.

Where mTLS is used

mTLS shows up wherever both endpoints are machines run by the same organisation or by contracted partners.

  • Service meshes. Sidecar proxies issue short-lived certificates to every workload and encrypt and authenticate all service-to-service traffic. Workload identity is usually carried in a URI SAN such as spiffe://example.com/ns/prod/sa/billing.
  • Partner and banking APIs. Open-banking and payment APIs commonly require clients to present a certificate issued by the provider or a designated CA.
  • OAuth 2.0. RFC 8705 defines two things: client authentication to the authorization server with a certificate (tls_client_auth for PKI-issued certificates, self_signed_tls_client_auth for self-signed ones), and certificate-bound access tokens, where the token carries a hash of the client certificate in its cnf claim (x5t#S256) so a stolen token is useless without the key.
  • Zero-trust access. Corporate devices hold a device certificate; the access proxy admits only connections that present one from the company CA.
  • IoT and devices. Each device gets a unique certificate at manufacture; the backend identifies the device by certificate rather than by a shared password baked into firmware.
  • Databases and message brokers. PostgreSQL, MySQL, Kafka and others support client certificate authentication natively.

Why mTLS belongs on a private CA

Use a private CA for client certificates. Publicly trusted certificates are being restricted to TLS server authentication, and even before that they were a poor fit: a public CA validates domain names, not your services or devices, and any customer of that CA could get a certificate that chains to the same root.

The policy changes as of November 2026:

Date Change Source
Oct 1, 2025 Let's Encrypt offers an opt-in tlsclient profile that keeps the clientAuth EKU for migration Let's Encrypt blog
Feb 11, 2026 Let's Encrypt's default classic profile stops including the TLS Client Authentication EKU Let's Encrypt blog
Jun 15, 2026 Chrome Root Program begins phasing out PKI hierarchies that violate its dedicated serverAuth rule; subordinate CAs disclosed from this date may assert only id-kp-serverAuth Chrome Root Program Policy 1.8
Jul 8, 2026 Let's Encrypt's tlsclient profile ends; no further certificates with clientAuth Let's Encrypt blog (updated March 16, 2026)
Mar 15, 2027 Subscriber certificates issued under Chrome-trusted roots must assert only id-kp-serverAuth Chrome Root Program Policy 1.8

Let's Encrypt's Generation Y certificates do not include client authentication at all. Other public CAs are moving the same way on their Chrome-trusted hierarchies. If an mTLS setup suddenly rejects freshly renewed certificates with errors about key usage or purpose, see client auth EKU removed.

The fix is to separate the two jobs:

Public web PKI (browsers trust it)        Private PKI (only your systems trust it)
----------------------------------        ----------------------------------------
Root (in browser root stores)             Your offline root CA
  └─ Intermediate (serverAuth only)         └─ Issuing CA "Client CA"
       └─ api.example.com                        ├─ billing-service   (clientAuth)
          EKU: serverAuth                        ├─ device-000123     (clientAuth)
                                                 └─ laptop-alice      (clientAuth)

Server presents the public cert to clients.   Server trusts ONLY the private Client CA
                                              when verifying client certificates.

Trusting only a dedicated client CA on the server is also the security property you want: a certificate from any other CA, public or private, is rejected. Build a private CA with OpenSSL walks through the commands.

Configuring mTLS in nginx

nginx verifies client certificates with ssl_verify_client and a CA bundle. The CA file listed in ssl_client_certificate is also sent to clients in CertificateRequest as the list of acceptable CA names.

server {
    listen 443 ssl;
    server_name api.example.com;

    ssl_certificate     /etc/ssl/api.example.com/fullchain.pem;   # public server cert
    ssl_certificate_key /etc/ssl/api.example.com/privkey.pem;

    ssl_client_certificate /etc/ssl/client-ca/ca-bundle.pem;      # private client CA(s)
    ssl_verify_client on;            # on | optional | optional_no_ca | off
    ssl_verify_depth  2;
    ssl_crl /etc/ssl/client-ca/client-ca.crl;                     # optional revocation list

    location / {
        proxy_set_header X-Client-Verify $ssl_client_verify;      # SUCCESS / FAILED:reason / NONE
        proxy_set_header X-Client-DN     $ssl_client_s_dn;
        proxy_set_header X-Client-Serial $ssl_client_serial;
        proxy_pass http://127.0.0.1:8080;
    }
}
  • on rejects connections without a valid certificate; nginx returns 400 No required SSL certificate was sent or 400 The SSL certificate error.
  • optional requests a certificate but lets the request through; check $ssl_client_verify in the application. Useful during rollout.
  • optional_no_ca accepts certificates that do not chain to your CA and leaves verification to the backend, which is rarely what you want.
  • Strip any incoming X-Client-* headers at the edge so clients cannot forge them.

Testing with curl and openssl s_client

Test both the failure and the success case. Without a client certificate, a TLS 1.3 server that requires one aborts after the client's Finished, so the error appears on the first read:

openssl s_client -connect api.example.com:443 -servername api.example.com -brief
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: CN=api.example.com
Verification: OK
Negotiated TLS1.3 group: X25519MLKEM768
...:error:0A00045C:SSL routines:ssl3_read_bytes:tlsv13 alert certificate required:...:SSL alert number 116

Without -brief, s_client also prints Acceptable client certificate CA names, which is the fastest way to see which CA the server expects. With the certificate and key:

openssl s_client -connect api.example.com:443 -servername api.example.com \
  -cert client.crt -key client.key -brief

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

If the client certificate is in a PKCS#12 file, curl accepts --cert-type P12 --cert client.p12:password; see PKCS#12 commands. Check that the client certificate really has the clientAuth purpose:

openssl x509 -in client.crt -noout -ext extendedKeyUsage
X509v3 Extended Key Usage:
    TLS Web Client Authentication

More connection tests are collected in testing TLS connections.

Common mTLS failures

Most mTLS failures come from a mismatch between what the server trusts and what the client sends, not from the protocol. Match the symptom to the cause:

Symptom Likely cause Fix
alert certificate required (116) on the client Client sent no certificate: wrong path, key missing, or the client library never loaded it Pass both certificate and key; for browsers, install the PKCS#12 bundle in the OS or browser store
alert unknown ca (48) Client certificate does not chain to a CA in the server's trust list Add the issuing CA (and intermediates) to the server's client CA bundle, or send the intermediate from the client
alert unsupported certificate or purpose errors Client certificate lacks the clientAuth EKU, typically a renewed public certificate Issue client certificates from a private CA with extendedKeyUsage=clientAuth
Browser shows no certificate picker Server's certificate_authorities list does not match any installed certificate's issuer Check Acceptable client certificate CA names in s_client output
Works on HTTP/1.1, fails on HTTP/2 Application relies on post-handshake (per-path) client authentication Request the certificate during the handshake, or use a dedicated hostname
Fails only behind a load balancer TLS terminates at the balancer, so the backend never sees the certificate Verify at the balancer and forward the verified identity in a header the edge controls

When a client certificate chain is the problem, the same tools you use for server chains apply; see verifying certificate chains.

Rotation and revocation

Plan for certificate renewal from the start, because client certificates are spread across many machines. The patterns that hold up:

  • Short lifetimes plus automation. Service meshes rotate workload certificates every few hours or days, which makes revocation almost unnecessary. Internal ACME servers or a mesh's built-in CA can do the same for VMs.
  • Overlap CA bundles during CA rotation. Add the new client CA to ssl_client_certificate first, then start issuing from it, and remove the old CA only after every old certificate has expired.
  • Keep a revocation path for long-lived device certificates. Publish a CRL and load it with ssl_crl, or keep a deny list in the application keyed by serial number or fingerprint.
  • Generate keys on the client. Have each device or service create its key and send a CSR; never email private keys around.
  • Monitor expiry. An expired client certificate looks like an outage on the client side, often in a system nobody remembers owning.

For the handshake itself in more depth, read the TLS 1.3 handshake; for EKU and key usage fields, see extended key usage and client certificate.

FAQ

What is the difference between TLS and mTLS?

In ordinary TLS only the server proves its identity with a certificate. In mutual TLS the server also sends a CertificateRequest, and the client must present its own certificate and prove it holds the matching private key with a CertificateVerify signature. Both sides end up authenticated before any application data flows.

Can I use a Let's Encrypt certificate as a client certificate?

Not for new certificates. Let's Encrypt removed the TLS Client Authentication EKU from its default profile on February 11, 2026 and stopped issuing it entirely after July 8, 2026; its newer Generation Y hierarchy does not include client authentication. Use a private CA for client certificates.

Why does my server say 'No required SSL certificate was sent'?

That is nginx's 400 response when ssl_verify_client on is set and the client did not present a certificate. Check that the client is configured with both the certificate and its private key, and that the certificate chains to a CA listed in ssl_client_certificate.

Does mTLS work with HTTP/2?

Yes, as long as the client certificate is requested during the initial handshake. RFC 8740 forbids TLS 1.3 post-handshake authentication on HTTP/2 connections, so servers cannot ask for a client certificate later, for example only on one URL path.

Sources

  1. RFC 9846: The TLS Protocol Version 1.3 (obsoletes RFC 8446)
  2. RFC 8705: OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens
  3. RFC 8740: Using TLS 1.3 with HTTP/2
  4. Chrome Root Program Policy
  5. Let's Encrypt: Ending TLS Client Authentication Certificate Support in 2026
  6. nginx: ngx_http_ssl_module
  7. OpenSSL s_client manual