Skip to main content
RunBook Academy

Secrets, PKI & CertificatesVII · TLS for OperatorsTLS

Certificate validation as the client performs it

Intermediate⏱ ~24 minopensslcurl

What you'll learn

  • Order the checks a TLS client performs on a server certificate chain
  • Map each named verifier error to the specific check that produced it
  • Explain why chain construction failures mask validity and name failures
  • Reproduce a client validation result offline with the OpenSSL verifier

Prerequisites

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

Not yet marked complete on this device.

When a client rejects a certificate it has already run several distinct tests, and it reports only the first one that failed. That single detail explains most wasted time in TLS incidents. An engineer reads one error, fixes the thing it names, and finds a second error waiting behind it, because the first failure prevented the later checks from ever running.

The checks, in the order they happen

Validation splits into three groups. First the verifier constructs a chain from the presented certificates up to a certificate it already trusts. Then it applies the profile rules of RFC 5280 to every certificate in that chain: signature, validity window, basic constraints, key usage. Only then does the application ask its own question, which RFC 5280 never covers: does this certificate speak for the name I actually asked for?

flowchart TD
    A["server sends leaf\nplus any intermediates"] --> B{"chain reaches a\ntrusted anchor?"}
    B -- "no local issuer" --> E20["error 20\nunable to get local issuer certificate"]
    B -- "anchor not trusted" --> E2["error 2\nunable to get issuer certificate"]
    B -- "yes" --> C{"every signature and\nvalidity window good?"}
    C -- "notAfter passed" --> E10["error 10\ncertificate has expired"]
    C -- "notBefore not reached" --> E9["error 9\ncertificate is not yet valid"]
    C -- "yes" --> D{"leaf suitable for\nthe intended purpose?"}
    D -- "no serverAuth" --> E26["error 26\nunsuitable certificate purpose"]
    D -- "yes" --> F{"requested hostname\nin subjectAltName?"}
    F -- "no" --> E62["error 62\nhostname mismatch"]
    F -- "yes" --> OK["accepted"]

The flow is a gate sequence, not a scoring system. Nothing is weighed against anything else. The first gate that closes produces the result, and every gate behind it stays untested. That is why a certificate can be simultaneously expired, wrongly named and unchained, and still produce exactly one error message.

Reproducing a client decision offline

The OpenSSL verifier applies the same checks a client applies, which makes it the fastest way to establish what is wrong without waiting for a browser or an application library to tell you. Give it the trust anchor, any intermediates the server sent, and the leaf:

openssl verify -CAfile root.crt -untrusted srv-ca.crt app.crt
openssl verify -CAfile root.crt -untrusted srv-ca.crt -purpose sslserver app.crt
openssl verify -CAfile root.crt -untrusted srv-ca.crt -verify_hostname app.lab.example app.crt

Each of those prints app.crt: OK when the corresponding check passes. The failures are more useful, and they are precise:

What is wrongVerifier output
The server did not send the intermediateerror 20 at 0 depth lookup: unable to get local issuer certificate
The chain leads to a root you do not trusterror 2 at 1 depth lookup: unable to get issuer certificate
The certificate is past its notAftererror 10 at 0 depth lookup: certificate has expired
The certificate is before its notBeforeerror 9 at 0 depth lookup: certificate is not yet valid
The certificate signs itselferror 18 at 0 depth lookup: self-signed certificate
The extended key usage does not allow this roleerror 26 at 0 depth lookup: unsuitable certificate purpose
The name asked for is not in the SANerror 62 at 0 depth lookup: hostname mismatch

Note the depth value in each line, because it tells you which certificate failed. Depth 0 is the leaf. Depth 1 is its issuer. Error 2 at depth 1 means the intermediate was present and readable and its own issuer was the problem, which is a completely different remediation from error 20 at depth 0.

Now the part that costs people an afternoon. Take a certificate that is genuinely expired and verify it against a trust store that does not contain its issuer:

CN=app.lab.example
error 20 at 0 depth lookup: unable to get local issuer certificate
error leaf-expired.crt: verification failed

The expiry is invisible. The verifier could not build a chain, so it never reached the validity check. If you fix the trust store and re-run, error 10 appears and looks like a new fault. It was there the whole time.

The name check is not part of path validation

The hostname test is the check people assume is automatic, and it is the one that is not. RFC 5280 path validation says nothing about DNS names being compared to anything. It is the application, guided by RFC 9525, that compares the name it intended to reach against the subjectAltName entries of the leaf. OpenSSL reflects this exactly: the verifier performs no hostname comparison at all unless you pass -verify_hostname, which is why a certificate for the wrong host can return OK from a careless invocation.

RFC 9525, which replaced RFC 6125, removed the Common Name fallback outright. Its rule is that the Common Name relative distinguished name must not be used to identify a service, and neither may any other relative distinguished name in the subject. A certification authority may still emit a Common Name, and the CA/Browser Forum Baseline Requirements require it to be a character-for-character copy of one of the SAN entries when present, but a client must ignore it. If your service works in one client and fails in another, and the difference is that the name appears only in the Common Name, the client that fails is the correct one.

A real client reports the same failure in its own words. Where the verifier says hostname mismatch, curl says:

curl: (60) SSL: no alternative certificate subject name matches target hostname 'wrong.lab.example'

That wording is a gift, because it names the target hostname that was actually used. In an incident where a load balancer, a service mesh sidecar or a proxy rewrites the host, that string tells you what the client asked for rather than what you assumed it asked for.

Production discipline

  1. Always supply the purpose and the hostname when verifying. A bare verification answers a narrower question than a client asks, and a pass from it means very little.
  2. Capture the chain the server sent and the client’s trust store in the same breath. The identical error text arises from a server omission and from a client gap, and only that pair distinguishes them.
  3. Read the depth, not just the error. Depth 0 problems live in the leaf and are fixed by reissuance or configuration; deeper problems live in the CA chain and usually affect every service issued by it.
  4. Treat a disabled verification flag in committed code as an incident, not a style issue. It is a permanent, silent removal of the authentication guarantee for every request that code makes.

Cross-course references

  • Linux for Production Sysadmins - Part XXIV (Time) covers clock synchronisation, and a host whose clock has drifted produces error 9 or error 10 from a certificate that is perfectly valid.
  • Kubernetes for Production Sysadmins - Part CXXII (DNSTroubleshoot) covers name resolution inside a cluster, which determines the hostname a workload asks for and therefore the name that must appear in the subjectAltName.
  • Observability for Production Sysadmins - Part LXIV (TLSMonitoring) covers probes that record which validation stage failed, so the depth and error number are already in the alert.

Quiz

Knowledge check · 4 questions

  1. Q1. A verification returns error 20 at depth 0, unable to get local issuer certificate. What does that tell you about the certificate's validity dates?

  2. Q2. An OpenSSL verification can return OK for a certificate whose subjectAltName does not contain the hostname you intend to connect to.

  3. Q3. Two different faults produce the message unable to get local issuer certificate. Name both, and name the evidence that separates them.

  4. Q4. Decide what actually failed and in what order to fix it.

    At 14:40 UTC an internal batch job that calls internal.example.com begins failing. The job host reports unable to get local issuer certificate. A colleague has already added the root to the job host trust store, restarted the job, and now reports a different error mentioning the hostname. The certificate was replaced by an automated renewal at 03:00 UTC the same day.

Passing score: 75%. Answers are checked in this browser.