"Certificate error" has about five distinct causes and the error message rarely names the right one. Two commands separate them.
1. Read what a server presents
echo | openssl s_client -connect example.com:443 \
-servername example.com 2>/dev/null \
| openssl x509 -noout -subject -issuer -datesFour facts: who it is for, who signed it, and the validity window. -servername sets SNI — the hostname sent _before_ the certificate is chosen. Omit it against a host serving many sites and you get whichever certificate is the default, which produces a mismatch error that has nothing to do with the real problem.
Verify
2. Check expiry as a yes-or-no
echo | openssl s_client -connect example.com:443 \
-servername example.com 2>/dev/null \
| openssl x509 -noout -checkend 604800 \
&& echo "valid for at least 7 more days" \
|| echo "EXPIRES WITHIN 7 DAYS"-checkend <seconds> exits zero or non-zero, which makes it usable in a monitoring check. This is the one to put in a cron job or a timer: certificate expiry is the most predictable outage in this profession and still one of the most common.
Verify
3. The SAN is what is actually validated
echo | openssl s_client -connect example.com:443 \
-servername example.com 2>/dev/null \
| openssl x509 -noout -ext subjectAltNameClients have validated against the Subject Alternative Name extension, not the Common Name, for years. A certificate whose CN matches but whose SAN does not will be rejected — and reading only the CN is why the error looks wrong.
Note how wildcards work: *.example.com covers www.example.com but not example.com itself, and not a.b.example.com. One label, no apex. That asymmetry is behind a large share of certificate mismatches.
Verify
4. Read the chain, and find the incomplete-chain bug
echo | openssl s_client -connect example.com:443 \
-servername example.com -showcerts 2>/dev/null \
| grep -E "^ *[0-9]+ s:|^ *i:"A server must send its own certificate and any intermediates. Only the root needs to be in the client's trust store. When a server sends the leaf alone, browsers often still work — they cache intermediates from previous sites — while curl and your application fail. That is the "works in my browser, fails in CI" report, and it is a server misconfiguration every time.
Verify the chain explicitly:
echo | openssl s_client -connect example.com:443 \
-servername example.com -verify_return_error 2>&1 \
| grep -E "Verify return code|verify error"Verify return code: 0 (ok) is what you want. A non-zero code names the failure: 10 is expired, 18 is self-signed, 20 is "unable to get local issuer certificate" — the incomplete chain.
Verify
5. Protocol and cipher
echo | openssl s_client -connect example.com:443 \
-servername example.com 2>/dev/null \
| grep -E "Protocol|Cipher"
openssl s_client -connect example.com:443 -servername example.com \
-tls1_1 </dev/null 2>&1 | tail -n 3The first shows what was negotiated — TLS 1.3 on anything modern. The second tries to force TLS 1.1 and should fail: it has been deprecated and disabled since 2021. If it _succeeds_ against one of your own servers, that is a finding.
Verify
6. Make a self-signed certificate and see the error from the inside
mkdir -p ~/labs/tls && cd ~/labs/tls
openssl req -x509 -newkey rsa:2048 -sha256 -days 3 -nodes \
-keyout server.key -out server.crt \
-subj "/CN=lab.local" \
-addext "subjectAltName=DNS:lab.local,DNS:localhost"
openssl x509 -in server.crt -noout -subject -issuer -dates -ext subjectAltNameSubject and issuer are the same name — that is what "self-signed" means, and error code 18.
Serve it and connect:
openssl s_server -accept 8443 -cert server.crt -key server.key -quiet &
sleep 1
curl -sS https://localhost:8443 2>&1 | head -n 3
curl -sS --cacert server.crt https://localhost:8443 2>&1 | head -n 3
kill %1 2>/dev/nullThe first fails: the signer is not in the trust store. The second succeeds, because you told curl to trust that specific certificate. That is the correct way to work with an internal CA — trust the CA explicitly. curl -k also "works", and it disables verification entirely, which means it is not a fix but a decision to stop checking.
Verify
The five causes, and how to tell them apart
| Symptom | Cause | Check |
|---|---|---|
certificate has expired | Expiry | -checkend 0 |
hostname mismatch | Name not in SAN | -ext subjectAltName |
unable to get local issuer | Incomplete chain (20) | -showcerts |
self-signed certificate (18) | Internal CA not trusted | --cacert |
| Works in browser, fails in curl | Incomplete chain | -showcerts, not the client |
Clean up
cd ~ && rm -rf ~/labs/tlsWhere this goes next
That is the vocabulary: addresses, prefixes, names, ports, routes, and TLS. The challenge puts a machine in front of you with two of them broken at once.