Skip to main content
RunBook Academy

Secrets, PKI & CertificatesVIII · TLS TroubleshootingTroubleshooting

curl exit 60 and the three different failures behind it

Intermediate⏱ ~22 mincurlopenssl

What you'll learn

  • Separate curl transport failures from curl certificate verification failures by exit status
  • Recognise that one exit status covers several distinct certificate defects
  • Disambiguate an incomplete served chain from a missing local trust anchor
  • Explain why the hostname failure message refers to subject alternative names

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.

curl is usually the first thing anyone runs, and its output is usually the first thing anyone misreads. It compresses several unrelated certificate faults into a single exit status, prints a sentence whose wording depends on how it was built, and gives no indication of which side of the connection owns the problem. All three limitations are workable once you know they exist.

Read the status, then read the sentence

Before anything certificate-shaped is considered, the exit status separates the layers established earlier. A name that does not resolve exits 6. A connection that is refused exits 7. A connection that hangs exits 28. A handshake that collapses for a protocol reason exits 35. Only 60 is a verification verdict: the peer presented a certificate and the client declined to authenticate it.

Capture the status deliberately, because a shell pipeline will happily discard it.

HOST=api.example.com

curl -sS -o /dev/null "https://$HOST/"
STATUS=$?
printf 'curl exit status: %s\n' "$STATUS"

Everything from here concerns status 60. That number is stable across curl versions and TLS backends. The sentence beside it is not, and treating the sentence as the identifier is how teams end up searching for a phrase that their own colleague on a different distribution never sees.

One neighbouring status is worth separating out now, because it is regularly mistaken for a TLS problem. Adding the fail option makes curl exit 22 when the server returns an HTTP error status, and that outcome means the opposite of a certificate fault: the handshake completed, the certificate verified, the request was delivered, and the application answered unhappily. A pipeline that treats 22 and 60 as one class of failure will retry a perfectly authenticated connection against an application that is telling it something specific.

Three faults, three sentences, one number

These three lines were produced against real services. Read them as a set, because the set is the lesson.

curl: (60) SSL certificate OpenSSL verify result: unable to get local issuer certificate (20)
curl: (60) SSL: no alternative certificate subject name matches target hostname 'wrong.lab.example'
curl: (60) SSL certificate problem: unable to get local issuer certificate

The first came from a server that transmitted its leaf certificate and nothing else, on a client whose trust store did contain the root. The second came from a request for a name that the certificate does not carry. The third came from a container whose trust store had never been given the private root, talking to a server that was sending a complete chain.

Now the uncomfortable part. A fourth capture, taken against a correctly configured server from a host that simply lacked the anchor, produced a line character-for-character identical to the first. The same wording, the same numeric detail, two entirely different defects with two different owners and two different fixes.

Why one message covers two defects

The client does not verify the list it received in isolation. It searches for a path from the leaf upwards, drawing candidates from two pools at once: the certificates that arrived on the wire, and the certificates already in its own store. Success means the union of those two pools contained a complete route to a trusted anchor. Failure means it did not.

That is why the message cannot name a culprit. From inside the client, an intermediate the server forgot to send and an intermediate the client was never given are the same absence.

flowchart TD
    A["curl exits 60"] --> B{"does the message name a hostname?"}
    B -- "yes" --> C["identity failure: read the SAN entries"]
    B -- "no" --> D["path failure: count what the server sent"]
    D --> E{"did the server send more than the leaf?"}
    E -- "no" --> F["the server chain file is incomplete"]
    E -- "yes" --> G["the local anchor set is incomplete"]

The decision has exactly one input the message does not give you: how many certificates the server actually transmitted. That is a single command away.

HOST=api.example.com
ANCHORS=/etc/ssl/certs/ca-certificates.crt

openssl s_client -connect "$HOST:443" -servername "$HOST" \
  -showcerts </dev/null 2>&1 | grep -c 'BEGIN CERTIFICATE'

curl -sS --cacert "$ANCHORS" -o /dev/null "https://$HOST/"
printf 'result with the nominated anchors: %s\n' "$?"

A count of one means the server sent the leaf alone, and unless that leaf was issued directly by a root you trust, the server is under-sending. A count of two or more with a still-failing curl points at the anchor set. The second command asks the question against a nominated bundle rather than whatever the process happens to inherit, which matters when the failing consumer is a container that ships its own.

The hostname message names the extension for a reason

The identity failure reads differently on purpose. It complains that no alternative certificate subject name matches the target, and that phrase is the subject alternative name extension, not the subject field.

This distinction is now the whole of the rule. Modern service identity verification requires clients to match the requested name against SAN entries and forbids falling back to the Common Name relative distinguished name, or to any other component of the subject, to identify a service. A certificate whose only occurrence of the hostname is in its Common Name will therefore fail with this message while a human inspecting the certificate insists the name is right there. It is, and it does not count.

Wildcards are the other reliable source of this message. A wildcard entry may only occupy the leftmost label, and it substitutes for exactly one label: a certificate covering all direct children of a domain does not cover the bare domain itself, and it does not cover a grandchild two levels down. A service that quietly acquired an extra label in its name during a platform migration will produce this failure with a certificate that everybody agrees was correct last week.

The practical consequence is that the fix for this message is never a client setting. Either connect using a name the certificate carries in its SAN, or reissue the certificate with the name added.

From a failed request to the next command

What curl printedThe question it leaves openThe command that answers it
exit 6 or 7Is this TLS at all?getent hosts then a plain TCP connection attempt
exit 35Did any version get agreed?s_client, reading the protocol line
60 with a hostname phraseWhich names does the certificate carry?openssl x509 -noout -ext subjectAltName on the served leaf
60 with a local issuer phraseHow many certificates arrived?s_client with -showcerts, counting the PEM blocks
60 that clears with --cacertWhose anchor set is wrong?compare the nominated bundle with the process default

Working the table in order costs less than a minute and produces a statement that survives review: not “TLS is broken”, but “the server transmits one certificate, the issuer named in it is our issuing CA, and that CA is not an anchor on this client”.

Production discipline

  1. Branch on the status, not the sentence. Automation that greps for message text will break on a distribution upgrade that changed nothing about your certificates.
  2. Count the transmitted certificates before assigning blame. It is the one measurement that separates the two defects sharing a message.
  3. Reproduce with --cacert against the failing consumer’s bundle. A success from your shell proves only that your shell is fine.
  4. Treat a hostname complaint as a certificate content problem. No client-side setting is the correct answer, and reaching for one is how estates acquire permanent exceptions.
  5. Keep the exact line. Paraphrasing a message into a ticket loses the numeric detail that identifies the fault.

Cross-course references

  • Linux for Production Sysadmins - Part LXXI (TLS) covers installing and refreshing the operating system anchor bundle that --cacert is standing in for during an investigation.
  • Kubernetes for Production Sysadmins - Part CXXI (ServiceTroubleshoot) covers the same status-first triage where the connection also crosses a Service and possibly a sidecar proxy before reaching an endpoint.
  • Observability for Production Sysadmins - Part LXIII (Synthetic) covers running this request continuously from several vantage points so that a per-member fault is visible before a human reports it.

Quiz

Knowledge check · 4 questions

  1. Q1. Two hosts both report exit 60 with a message about being unable to get a local issuer certificate. What single measurement separates the two possible causes?

  2. Q2. A certificate that carries the requested hostname only in its Common Name will still fail modern hostname verification.

  3. Q3. Why is branching on the curl exit status more reliable than branching on the printed message?

  4. Q4. Assign the defect to an owner using only the evidence available.

    A batch job in a container fails against internal.example.com with exit 60 and a message about being unable to get a local issuer certificate. The same request from the container host succeeds. The service owner points out that nothing on the server has changed for six weeks, and the container image was rebuilt yesterday.

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