Secrets, PKI & CertificatesVIII · TLS TroubleshootingTroubleshooting
The classic failures and their exact signatures
What you'll learn
- Map each verified verification error to exactly one defect and one owner
- Use the reported depth to distinguish a missing intermediate from a missing anchor
- Run a four-command triage that separates path, name, purpose and validity
- Recognise the two failures that are not caused by the certificate at all
Prerequisites
Practice
Verified against OpenSSL 3.5.x teaching target; 3.0+ minimum · OpenSSH 10.x teaching target; 8.2+ minimum for certificate workflows · OpenBao 2.6.x · Smallstep step-ca 0.30.x · Certbot / Pebble Certbot current release; Pebble 2.10.x ACME test server · Kubernetes (cross-course target) 1.36.x · PostgreSQL 17.x · 2026-08-26
Almost every TLS incident an infrastructure team meets belongs to one of six classes. Each has a signature that can be read off a captured verification without argument, and each has a different owner. The value of a table is not that it saves thinking; it is that at three in the morning it prevents the wrong thinking from running unchecked for forty minutes.
Two numbers, and only then a sentence
Verification output carries a numeric error and a depth. Both are stable, and together they are almost always sufficient. The depth is the position in the chain of the certificate that failed, counting from zero at the leaf and increasing towards the anchor. The error number identifies what was wrong with the certificate at that position.
Run the checks separately so that each answer is unambiguous. A single combined command that fails tells you something is wrong; four commands tell you which thing.
LEAF=/etc/ssl/certs/api.example.com.pem
CHAIN=/etc/ssl/certs/api-example-com-intermediates.pem
ANCHORS=/etc/ssl/certs/ca-certificates.crt
NAME=api.example.com
openssl verify -CAfile "$ANCHORS" -untrusted "$CHAIN" "$LEAF"
openssl verify -CAfile "$ANCHORS" -untrusted "$CHAIN" \
-verify_hostname "$NAME" "$LEAF"
openssl verify -CAfile "$ANCHORS" -untrusted "$CHAIN" \
-purpose sslserver "$LEAF"
openssl x509 -in "$LEAF" -noout -checkend 0
Path, name, purpose, validity. Four questions, four answers, no interference between them.
The decision table
| Verified evidence | Class | Where the defect is | First corrective action |
|---|---|---|---|
Certificate will expire from openssl x509 -checkend 0, exit 1 | Expired or outside its window | The certificate, or the clock reading it | Renew and reload, after confirming the clock |
error 62 at 0 depth lookup: hostname mismatch | Wrong name | The certificate content, not the trust configuration | Reissue with the name in the SAN, or use a name it carries |
error 20 at 0 depth lookup: unable to get local issuer certificate | Missing intermediate | The server, if it under-sends; the client store otherwise | Serve the full chain file, then re-measure |
error 2 at 1 depth lookup: unable to get issuer certificate | Unknown anchor | The verifying client’s trust store | Install the correct anchor, or fix which CA issued the leaf |
error 26 at 0 depth lookup: unsuitable certificate purpose | Wrong purpose or EKU | The issuance request or the CA profile | Reissue with the extended key usage the role requires |
error:0A0000BF:SSL routines:tls_setup_handshake:no protocols available | Version floor | The party that refused, which may be your own client | Determine which side refused before touching either |
Every string in the left column is real output, captured from OpenSSL 3.5 against a working two-tier laboratory PKI and a live web server. The last row is the odd one out, and the section on purpose and protocol below explains why.
Error 20 and error 2 name different missing certificates
These two rows are the ones teams conflate, and the confusion is expensive because the owners differ.
Verifying a leaf against a root without supplying the intermediate produces this:
error 20 at 0 depth lookup: unable to get local issuer certificate
Depth zero is the leaf. Nothing anywhere could issue it, because the intermediate was in neither pool. On a live connection the equivalent trace shows the same number followed by a second, consequential one:
depth=0 CN=app.lab.example
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 CN=app.lab.example
verify error:num=21:unable to verify the first certificate
verify return:1
Now verify the same leaf while nominating the intermediate itself as the only anchor:
error 2 at 1 depth lookup: unable to get issuer certificate
The depth moved to one. Path building got past the leaf, because this time the intermediate was available, and then stopped because the intermediate’s own issuer could not be found and an intermediate is not a self-signed anchor. The chain is one certificate longer, and the gap is one level higher.
Read it as a rule. The depth tells you how far up the chain the client got before it ran out of certificates, and therefore which certificate is missing. Depth zero means the intermediate is absent. A depth above zero means an anchor is absent, or the wrong anchor was nominated.
flowchart TD
A["verification failed with a missing-issuer error"] --> B{"what depth is reported?"}
B -- "depth 0" --> C["the leaf has no available issuer"]
B -- "depth 1 or higher" --> D["an intermediate has no available issuer"]
C --> E{"does the server send more than the leaf?"}
E -- "no" --> F["server chain file: add the intermediate"]
E -- "yes" --> G["client store: the intermediate is not usable"]
D --> H["anchor set: install or correct the root"]
The diagram is the same rule drawn once. Start from the depth, then ask a single question about what arrived on the wire, and the branch you land on names both the fix and the team that makes it.
Purpose and protocol: the two rows that are not about trust
The fifth row is a genuinely different failure. A certificate can be inside its validity window, carry the right name and chain perfectly to a trusted anchor, and still be refused:
error 26 at 0 depth lookup: unsuitable certificate purpose
This is the extended key usage extension doing its job. A leaf marked only for TLS Web Server Authentication is not usable as a client certificate, and asking it to be one produces exactly this. In practice the row appears when a mutual TLS deployment reuses a server certificate for the client side, or when an automated issuance profile was copied from a server role to a client role without changing the usage. The fix is a reissue with the correct usage, and it is worth knowing that a catch-all usage is not an option in the public trust regime, where the any extended key usage value must not appear in a subscriber certificate.
The sixth row is more subtle still. Pinning an obsolete version against a modern OpenSSL build produces this:
error:0A0000BF:SSL routines:tls_setup_handshake:no protocols available
Nothing was ever sent. The local library declined to offer that version at its own configured security level, so this string is evidence about the client that printed it. Reporting it as proof that a server has disabled a version is a common and confident mistake. Establish which side refused, by testing the same server from a client that will offer the version, before anyone edits a server configuration.
Working the table under pressure
Discipline beats recall here. Capture, then classify, then act, and say the class out loud before proposing a change.
Two traps recur. The first is a certificate that is correct in every respect but belongs to a different service, which produces the hostname row while everyone hunts for a trust problem; the SAN entries settle it in one command. The second is a chain error that appears on some connections and not others, which is a pool with one inconsistent member and is invisible if you only ever test the name rather than each address.
The validity row also has a second face that the table does not show, because the tooling reports it far less clearly. A certificate can be outside its window at the other end, not yet having reached its start instant, and that happens whenever the verifying host runs behind the issuer. The signature is the same shape as expiry and the cause is entirely different, which is why the corrective action in that row begins with confirming the clock rather than with renewing. The next lesson treats that case on its own.
Finally, write the class into the incident record in the same words every time. Missing intermediate, unknown anchor, wrong name, outside window, unsuitable purpose, version floor. Six labels used consistently across a year of tickets turn an unreadable pile of TLS incidents into a distribution you can act on, and the distribution is usually lopsided in a way that tells you exactly which control is missing.
Production discipline
- Classify before you change. A named class turns an open argument into a bounded task with an owner.
- Read the depth before the sentence. It is the only part of a missing-issuer error that identifies which certificate is absent.
- Test path, name, purpose and validity separately. A combined check reports whichever failure came first and conceals the rest.
- Do not accept a protocol error as server evidence. Prove which side refused, or you will change the wrong one.
- Re-measure after the fix with the identical command. The pair of captures is what makes the incident record defensible.
Cross-course references
- Linux for Production Sysadmins - Part LXXI (TLS) covers where a distribution keeps served chain files and anchor bundles, which is where four of these six rows are repaired.
- Kubernetes for Production Sysadmins - Part LXXVI (Certs) covers a cluster PKI whose components fail with these same errors, and where the purpose row appears routinely because client and server roles use separate issuance profiles.
- Observability for Production Sysadmins - Part LXIV (TLSMonitoring) covers alerting on the validity row before it becomes an outage, which is the only row on this table that is reliably preventable in advance.
Quiz
Knowledge check · 4 questions
Q1. A verification reports an unable-to-get-issuer error at depth 1. What does the depth tell you?
Q2. A message reporting that no protocols are available is reliable evidence that the server has disabled that TLS version.
Q3. Why should validity and hostname be checked with their own commands rather than relying on a single combined verification?
Q4. Classify the failure and name the owner before any change is proposed.
At 03:12 UTC a service mesh sidecar on web-01 begins rejecting connections to an internal API. A captured verification shows an unsuitable certificate purpose error at depth 0. The certificate chains cleanly, is well inside its validity window, and carries the correct name in its SAN. The platform team deployed a new issuance profile the previous afternoon.
Passing score: 75%. Answers are checked in this browser.