Part VII: Beyond the browser · Chapter 23
Both sides show ID: mutual TLS
In one sentence
In mutual TLS the client also proves who it is, with a certificate and a signature from its private key, so both ends are authenticated before any data flows.
Who is this for
Level: Practitioner
On this page
The idea
- Beginner
When you open a website over HTTPS, only one side shows ID. The server sends its certificate and proves it holds the matching private key. Your browser checks all of that. The server, on the other hand, has no idea who you are yet. You prove that later, inside the encrypted connection, with a password, a cookie or a token.
That works well for people. It works less well for machines. A payment service that talks to a bank, a sensor that reports to a cloud platform, or one microservice that calls another: none of them can type a password. Each needs a way to prove its identity that cannot be guessed, phished or copied out of a log file.
Mutual TLS (mTLS) gives them one. It is ordinary TLS with one extra step: the server asks the client for a certificate too. The client sends its own client certificate and then proves it holds the matching private key by signing part of the handshake. Now both sides know who is on the other end, and they know it before a single byte of application data is sent.
Three things are worth remembering from the start.
- The certificate alone proves nothing. A certificate is public. Anyone can copy it. The proof is the fresh digital signature the client makes with its private key during this handshake. That is why client private keys must be guarded as carefully as server keys.
- The server decides whom it trusts. It does not accept any certificate at all. It accepts only certificates issued by the certificate authorities it was told to trust for clients. Usually that is a single CA run by the organisation itself, a private CA.
- mTLS answers “who is this?”, not “what may they do?”. After the handshake, the server still has to decide what this client is allowed to do. That is authorisation, and it is a separate step.
Where do you meet mTLS? Inside data centres and Kubernetes clusters, where a service mesh gives every service its own certificate. In banking and payment APIs. In company Wi-Fi and VPNs, where laptops prove they are company devices. And in many Internet of Things platforms, where each device carries a certificate from the factory (chapter 25, device identity).
How it really works
- Developer
- Expert
How does the server ask for a client certificate?
In TLS 1.3 (RFC 9846), client authentication adds three things to the normal handshake:
- CertificateRequest. Right after its EncryptedExtensions message, the server sends a CertificateRequest (RFC 9846, section 4.4.2). It must list the signature algorithms the server accepts. It may also carry a
certificate_authoritiesextension (section 4.3.4) with the names of the CAs it trusts for clients, so a client with several certificates can pick the right one. - Certificate. After the server’s Finished message, the client sends its certificate and any intermediate certificates (section 4.5.1).
- CertificateVerify. The client signs a hash of the whole handshake so far with its private key (section 4.5.2). This is the actual proof. The signature covers this connection’s random values and key shares, so it cannot be replayed on another connection.
The client then sends its Finished message, and both sides start sending application data.
Two details matter in practice:
- What if the client has no certificate? It sends an empty Certificate message. The specification lets the server choose: carry on without client authentication, or stop with a
certificate_requiredalert. Strict servers stop. That alert is what tools report as “alert certificate required”. - Who can see the client certificate? In TLS 1.3 everything after the ServerHello is encrypted, including the client’s certificate. In TLS 1.2 the client certificate travelled in the clear, so anyone watching the network could read which client connected.
TLS 1.3 also defines post-handshake authentication (section 4.7.2): a server can ask for a client certificate later in a connection, for example only when the client opens an admin page. Support for it varies between clients, so most deployments ask during the handshake.
What does the server check?
When the client certificate arrives, the server runs the same kind of path validation a browser runs on a server certificate, but against a different set of trust anchors:
- The chain. The certificate must chain to one of the trust anchors configured for clients. This is a small, deliberate list, usually your own client CA. It is not the operating system’s list of public roots.
- Dates and signatures. Every certificate in the chain must be within its validity period and correctly signed.
- The purpose. If the certificate has an extended key usage (EKU) extension, it must include
id-kp-clientAuth. RFC 5280, section 4.2.1.12 says that when the extension is present, the certificate may be used only for the purposes listed. - Revocation. For a private CA you control, revocation can actually work: the server can load your CRL and refuse revoked clients. This is one of the few places where hard-fail revocation checking is realistic.
- The identity. Finally the application maps the certificate to an identity: a name in the subject alternative name (a URI, an email address or a DNS name) or, in older systems, the subject’s distinguished name. Then it applies its authorisation rules.
Why not accept certificates from a public CA?
Because anyone can get one. For most certificates, a public CA checks only that the requester controls a domain name. If your API trusted “any certificate that chains to a public root”, it would let in everyone on the internet who owns a domain. You would then have to filter on names, and one mistake in that filter opens the door.
A dedicated client CA flips the default. Only clients you enrolled can get a certificate from it, so a valid chain already means “one of ours”. That is why the trust anchor for clients should be specific: your own CA, or one issuing CA that serves only your clients.
What is changing at public CAs?
Public TLS certificates are becoming server-only. For years many public CAs put both serverAuth and clientAuth in the EKU of every TLS certificate, and some organisations used those certificates for server-to-server mTLS. Chrome’s root program, in particular, now wants each public TLS hierarchy to serve one purpose:
- 15 June 2026: Chrome's root program starts phasing out root hierarchies that are not dedicated to TLS server authentication.
- 15 March 2027: TLS certificates that chain to the Chrome Root Store must list only the TLS server authentication purpose.
- 11 February 2026 for the default profile, and 8 July 2026 for the last opt-in profile: Let's Encrypt certificates no longer include the TLS client authentication purpose.
The reasoning is in the Chrome Root Program Policy, section 1.3.2: the Chrome Root Store is used only for TLS server authentication, and hierarchies dedicated to one purpose are simpler to govern and to change. The practical effect is that client authentication moves to private CAs or to dedicated client-authentication hierarchies. If your servers use their public certificates as client certificates towards a partner, those connections fail as soon as a renewed certificate arrives without clientAuth.
How do service meshes give every service an identity?
In a large cluster, issuing certificates by hand to hundreds of services is impossible. A service mesh automates it. A small proxy runs next to each service (or on each node), and the mesh’s control plane runs a CA. Each proxy generates a key pair, sends a CSR, receives a short-lived certificate and renews it automatically long before it expires. All traffic between services then goes over mTLS, without changing the services themselves.
SPIFFE (the Secure Production Identity Framework for Everyone) standardises what those identities look like. A SPIFFE ID is a URI such as spiffe://example.org/payments/api. In an X.509 certificate, called an X.509-SVID, it goes into the subject alternative name as a URI, and the X.509-SVID specification says there must be exactly one. Leaf SVIDs must have CA:FALSE and the digitalSignature key usage. Leaf SVIDs should carry an EKU, and when they do it must include both serverAuth and clientAuth, because every workload is both a client and a server. Istio, for example, gives each workload an ID of the form spiffe://<trust-domain>/ns/<namespace>/sa/<service-account>.
SPIRE is the reference implementation of SPIFFE. A SPIRE server holds the signing keys for a trust domain. An agent on every node first proves which machine it runs on (node attestation), then identifies each process that asks for an identity (workload attestation), and hands it the matching SVID through a local Workload API. The workload does not need a key file deployed alongside it.
At work
- Developer
- IT ops
How do I issue a client certificate?
Use a CA that exists only for clients. The certificate needs CA:FALSE, the digitalSignature key usage, the clientAuth EKU and a name your application understands. Put the extensions in a small file:
basicConstraints=critical,CA:FALSE
keyUsage=critical,digitalSignature
extendedKeyUsage=clientAuth
subjectAltName=URI:spiffe://example.org/ci/build-agent
The client makes its own key and a CSR, and the CA signs it. The private key never leaves the client:
# on the client
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
-keyout client.key -out client.csr -subj "/O=Example Corp/CN=build-agent-07"
# on the client CA
openssl x509 -req -in client.csr -CA client-ca.crt -CAkey client-ca.key \
-CAcreateserial -days 30 -extfile client.ext -out client.crt
Check that the result is fit for client authentication. openssl verify can test the purpose as well as the chain:
openssl verify -CAfile client-ca.crt -purpose sslclient client.crt
openssl verify -CAfile client-ca.crt -purpose sslclient srvonly.crt
client.crt: OK
CN=api.example.com
error 26 at 0 depth lookup: unsuitable certificate purpose
error srvonly.crt: verification failed
The second certificate chains to the same CA, but its EKU says serverAuth only. This is exactly what a renewed public TLS certificate looks like after the changes described above.
How do I test mTLS without a real server?
OpenSSL has a tiny test server. -Verify 1 makes the client certificate mandatory, and -CAfile sets the client trust anchor:
openssl s_server -accept 127.0.0.1:8443 -cert server.crt -key server.key \
-CAfile client-ca.crt -Verify 1 -www
Connect without a client certificate first. The server lists the CA names it accepts, then refuses (output shortened):
openssl s_client -connect 127.0.0.1:8443 -servername localhost -CAfile server.crt
Acceptable client certificate CA names
O=Example Corp, CN=Example Client CA
...
Verify return code: 0 (ok)
...
EC760000:error:0A00045C:SSL routines:ssl3_read_bytes:tlsv13 alert certificate required:../openssl-3.2.3/ssl/record/rec_layer_s3.c:909:SSL alert number 116
“Verify return code: 0 (ok)” is the client’s verdict on the server. The alert number 116 is certificate_required: the server’s verdict on the client. Add -cert client.crt -key client.key and the connection succeeds.
How do I call an mTLS API with curl?
With curl built on OpenSSL (as on most Linux systems), pass the certificate and key with --cert and --key. Without them, the request fails with the same alert:
curl --cacert server.crt https://localhost:9443/
curl --cacert server.crt --cert client.crt --key client.key https://localhost:9443/
curl: (56) OpenSSL SSL_read: OpenSSL/3.0.13: error:0A00045C:SSL routines::tlsv13 alert certificate required, errno 0
The second command returns the page. If your client certificate was issued by an intermediate CA, put the intermediate after the leaf in the --cert file so the server receives the full chain. curl builds that use Windows Schannel, such as the curl.exe that ships with Windows, read client certificates from the Windows certificate store instead of from files; see the curl manual for --cert.
How do I require client certificates in nginx?
The nginx SSL module does it with a few directives:
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/nginx/tls/api.example.com.crt;
ssl_certificate_key /etc/nginx/tls/api.example.com.key;
ssl_client_certificate /etc/nginx/tls/client-ca.crt; # client trust anchor only
ssl_verify_client on; # or "optional"
ssl_verify_depth 2;
ssl_crl /etc/nginx/tls/client-ca.crl;
location / {
proxy_set_header X-Client-Verify $ssl_client_verify;
proxy_set_header X-Client-DN $ssl_client_s_dn;
proxy_pass http://app;
}
}
ssl_client_certificatenames the CAs trusted for clients, and nginx sends their names to clients. Usessl_trusted_certificatefor extra chain certificates whose names you do not want to advertise.ssl_verify_client optionalasks for a certificate but lets clients without one through. Your application must then check$ssl_client_verify, which isSUCCESS,FAILED:reasonorNONE.- nginx answers a failed verification with its own status codes 495 and 496, which you can map to a friendly page with
error_page. - If TLS ends at a proxy, the backend only sees what the proxy forwards. Make sure the backend accepts these identity headers only from the proxy, never from the outside.
What usually breaks?
- The client sends no certificate. Often the wrong file was configured. Some clients also send nothing when their certificate was not issued by any CA in the server’s list. Compare the “Acceptable client certificate CA names” with your certificate’s issuer.
- The chain is incomplete. The server has only the root, and the client sent only its leaf. Send the intermediate from the client, or add it to the server’s client CA file.
- Wrong purpose. A certificate with
serverAuthonly fails with “unsuitable certificate purpose”. Re-issue it from your client CA withclientAuth. - Expiry. Client certificates expire too, often on hundreds of devices at once. Put them in your inventory and automate renewal, as a mesh does.
- A load balancer in the middle. If it terminates TLS, the backend never sees the client certificate. Either pass identity on as above or let the TLS pass through.
Paste a client certificate into the decoder and check its Extended Key Usage, Subject Alternative Name and issuer:
Common mistakes
- “mTLS means running TLS twice.” It is one handshake. The server adds a CertificateRequest, and the client adds a Certificate and a CertificateVerify.
- “Any certificate from a public CA is good enough for client authentication.” A public certificate only proves control of a domain. Trust a dedicated client CA instead, and check the name.
- “Sending the certificate proves who the client is.” Certificates are public. The proof is the CertificateVerify signature made with the client’s private key.
- “The client certificate encrypts the connection.” Certificates do not encrypt anything. In TLS 1.3 the traffic is protected with session keys from an ephemeral (EC)DHE exchange; the certificate keys only sign.
- “Our public TLS certificates will keep working as client certificates.” Public hierarchies are becoming server-only, and renewed certificates drop
clientAuth. Move client authentication to a private CA before the old certificates expire. - “With mTLS we no longer need authorisation.” mTLS tells you who is calling. Your application still decides what that caller may do.
Why it matters to the business
- Leader
Check yourself
A client sends its certificate during an mTLS handshake. What actually proves it is the real client?
RightNot quite The answer is B: The CertificateVerify signature made with the client's private key
- What happened
- The client signs the handshake transcript with its private key in CertificateVerify.
- Why
- A certificate is public and can be copied. Only the holder of the private key can make a valid signature over this connection's handshake.
- The rule
- A certificate says which key belongs to a name. A fresh signature proves you hold that key.
Show answer
Answer B The CertificateVerify signature made with the client's private key
- What happened
- The client signs the handshake transcript with its private key in CertificateVerify.
- Why
- A certificate is public and can be copied. Only the holder of the private key can make a valid signature over this connection's handshake.
- The rule
- A certificate says which key belongs to a name. A fresh signature proves you hold that key.
Your API should accept only your own build agents. Which trust anchor should the server use for client certificates?
RightNot quite The answer is B: A dedicated client CA that issues only to your agents
- What happened
- Trust a CA that issues only to clients you enrolled.
- Why
- Public CAs issue to anyone who controls a domain, so a valid public chain says nothing about being one of your agents.
- The rule
- Keep client trust anchors small and specific, and still check the name in the certificate.
Show answer
Answer B A dedicated client CA that issues only to your agents
- What happened
- Trust a CA that issues only to clients you enrolled.
- Why
- Public CAs issue to anyone who controls a domain, so a valid public chain says nothing about being one of your agents.
- The rule
- Keep client trust anchors small and specific, and still check the name in the certificate.
A partner connection that used your public TLS certificate as a client certificate fails after renewal with 'unsuitable certificate purpose'. Why?
RightNot quite The answer is B: The new certificate's EKU lists serverAuth only
- What happened
- The renewed public certificate no longer includes the clientAuth purpose.
- Why
- Chrome's root program is phasing in TLS hierarchies dedicated to server authentication, so CAs are removing clientAuth from their public TLS certificates.
- The rule
- Use a private or dedicated client CA for client authentication.
Show answer
Answer B The new certificate's EKU lists serverAuth only
- What happened
- The renewed public certificate no longer includes the clientAuth purpose.
- Why
- Chrome's root program is phasing in TLS hierarchies dedicated to server authentication, so CAs are removing clientAuth from their public TLS certificates.
- The rule
- Use a private or dedicated client CA for client authentication.
Go deeper
- RFC 9846, sections 4.4.2 and 4.5: the CertificateRequest, Certificate and CertificateVerify messages in TLS 1.3.
- RFC 5280, section 4.2.1.12: the extended key usage extension and the clientAuth purpose.
- Chrome Root Program Policy, section 1.3.2: why public TLS hierarchies must be dedicated to server authentication, with the dates.
- Let’s Encrypt: Ending TLS client authentication: the timeline and the reasons, with a later update.
- SPIFFE X.509-SVID specification and SPIRE concepts: workload identity in certificates, and how SPIRE attests and issues it.
- nginx
ngx_http_ssl_module: every directive and variable for client certificates. - RFC 8705: OAuth 2.0 with mutual TLS, including access tokens bound to a client certificate.
The crash course covers this chapter in lesson 10, Beyond the browser, and what comes next.
Early edition. Checked against the standards; an independent expert review is in progress. Found a mistake? Tell us.