Short answer
An mTLS handshake almost always fails for one of five reasons: the server does not trust the CA that issued the client certificate; the client certificate has expired; it is missing the clientAuth EKU; the client sends an incomplete chain; or there is a hostname mismatch on the server's certificate. Use openssl s_client and curl -v to see which side rejects, and why.
First step: which side rejects?
The key distinction is whether it is the server rejecting the client certificate, or the client rejecting the server's. Run with full debug:
curl -v --cert client.crt --key client.key \
--cacert ca.crt \
https://api.example.com/health
If you see SSL certificate problem on the server's certificate, the client's validation is failing. If you instead see alert unknown ca or alert handshake failure after the client certificate is sent, the server is rejecting your client certificate.
1. Wrong CA (the server does not trust the issuer)
The most common cause. The server validates client certificates against a specific CA (ssl_client_certificate in nginx), and your certificate is issued by a different one. Symptom: alert unknown ca. Check which CA issued your client certificate and compare with the server's accepted CAs:
# Issuer of your client certificate openssl x509 -in client.crt -noout -issuer # CA names the server accepts (look for "Acceptable client certificate CA names") openssl s_client -connect api.example.com:443 -servername api.example.com 2>/dev/null \ | grep -A20 "Acceptable client"
If the issuer does not match one of the accepted CA names, you have found the fault.
2. Expired client certificate
An expired client certificate is rejected just as firmly as an expired server certificate — but no browser warns you in advance. Check the dates:
openssl x509 -in client.crt -noout -dates
If notAfter is in the past, the certificate must be renewed. It is the most overlooked mTLS failure, precisely because client certificates are rarely monitored as carefully as server certificates.
3. Missing clientAuth EKU
A certificate must have its Extended Key Usage set to clientAuth to be usable as a client certificate. A certificate issued for a server (with serverAuth) is often rejected in the client role. Check:
openssl x509 -in client.crt -noout -text | grep -A1 "Extended Key Usage"
You should see TLS Web Client Authentication. If it is absent — or it only says TLS Web Server Authentication — the certificate must be reissued with the correct EKU.
4. Incomplete chain from the client
If your client certificate is issued via an intermediate, the client must send both the leaf and the intermediate so the server can build the chain up to its trusted CA. If the client sends only the leaf, you see unable to get local issuer certificate on the server side. This is the same root problem as on the server side — see "unable to get local issuer certificate" and the certificate chain explained. With curl you send the full chain in one file:
# client-fullchain.pem = leaf + intermediate(s)
curl --cert client-fullchain.pem --key client.key \
--cacert ca.crt https://api.example.com/health
5. Hostname mismatch on the server's certificate
Even in mTLS the client still validates the server's certificate normally. If the server's certificate does not cover the name you connect to, it fails — independently of the client certificate. It is a common CA-related error; see the full background in the TLS handshake. Check the server's SAN:
openssl s_client -connect api.example.com:443 -servername api.example.com 2>/dev/null \ | openssl x509 -noout -text | grep -A1 "Subject Alternative Name"
Decision table
| Symptom | Likely cause |
|---|---|
alert unknown ca | Wrong CA on the client certificate |
certificate has expired | Expired client certificate |
unsupported certificate purpose | Missing clientAuth EKU |
unable to get local issuer certificate | Incomplete chain |
handshake failure after cert sent | Server rejected the client identity |
Stop diagnosing expiry after the fact
Two of the five causes — an expired client certificate and an incomplete chain — are entirely predictable and tend to strike at the worst possible moment. CertControl monitors both server and client certificates, including those from internal CAs, validates that chains are complete, and warns days before a certificate expires. So you find the fault before the handshake fails — not afterwards with curl -v. Understand the basics in our mTLS guide.
Frequently asked questions
Why do I just get "handshake failure" with no details?
TLS alerts are deliberately sparse so as not to leak information. Run curl -v or openssl s_client for more, and first determine whether it is the server or the client rejecting — that halves the debugging.
What is the difference between clientAuth and serverAuth EKU?
They are two different values in the Extended Key Usage field. serverAuth allows the certificate to be used by a server, clientAuth by a client. An mTLS client certificate must have clientAuth; many systems reject a certificate that only has serverAuth.
Should the client send the whole chain or just its own certificate?
If the client certificate is issued via an intermediate, the client must send both the leaf and the intermediate, so the server can build the chain up to the CA it trusts. Otherwise you risk "unable to get local issuer certificate" on the server side.
How do I see which CAs an mTLS server accepts?
Run openssl s_client against the server and look for the Acceptable client certificate CA names block in the output. It lists the CAs whose client certificates the server will accept.
Can a wrong clock cause mTLS errors?
Yes. If the client's or server's clock drifts, a valid certificate can appear expired or not yet valid. Synchronise the clock via NTP before debugging further.