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

> How mTLS adds client certificate authentication to TLS 1.3, why it needs a private CA, and how Chrome and Let's Encrypt changes in 2026 affect it.

Source: https://ssl.sh/learn/mtls/
Last updated: 2026-11-10

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.

```text
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](https://ssl.sh/errors/client-auth-eku-removed/).

The fix is to separate the two jobs:

```text
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](https://ssl.sh/cookbook/private-ca/) 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.

```nginx
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:

```bash
openssl s_client -connect api.example.com:443 -servername api.example.com -brief
```

```text
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:

```bash
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](https://ssl.sh/cookbook/pkcs12/). Check that the client certificate really has the clientAuth purpose:

```bash
openssl x509 -in client.crt -noout -ext extendedKeyUsage
```

```text
X509v3 Extended Key Usage:
    TLS Web Client Authentication
```

More connection tests are collected in [testing TLS connections](https://ssl.sh/cookbook/test-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](https://ssl.sh/cookbook/verify-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](https://ssl.sh/glossary/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](https://ssl.sh/learn/tls-handshake/); for EKU and key usage fields, see [extended key usage](https://ssl.sh/glossary/extended-key-usage/) and [client certificate](https://ssl.sh/glossary/client-certificate/).
