curl (60) SSL certificate problem

Exit code 60 from curl means certificate verification failed, and the phrase printed after "SSL certificate problem" identifies which check failed rather than being part of one single error. "unable to get local issuer certificate" means the chain is incomplete or the root is untrusted; "self-signed certificate" means the chain terminates in a certificate no CA vouched for; "certificate has expired" means the validity dates have passed; and a subject-name message means the certificate is valid but was issued for a different hostname. curl verifies against a CA bundle chosen when it was compiled, which is why `curl -v https://example.com` printing the CAfile path is the first useful diagnostic. Passing `-k` silences all four by disabling verification entirely and should never survive past the diagnosis.

Updated August 19, 2026 Verified against OpenSSL 3.5.7

Exit code 60 is four different errors #

curl returns 60 — CURLE_PEER_FAILED_VERIFICATION — for any failure of certificate verification. Searching for the number alone mixes four unrelated problems together. Find your phrase in this table first:

Message after “SSL certificate problem” What failed Fix
unable to get local issuer certificate No path from the certificate to a trusted root Incomplete chain or untrusted root
self-signed certificate (older curl: self signed certificate) The certificate signed itself Self-signed certificate
self-signed certificate in certificate chain A CA in the chain is self-signed and untrusted Self-signed in chain
certificate has expired notAfter is in the past Expired certificate

One more message also exits 60 but is not printed as a “certificate problem” at all:

curl: (60) SSL: no alternative certificate subject name matches target host name 'example.com'

That is a hostname mismatch, and nothing about the chain is wrong.

First: which CA bundle is curl reading? #

Almost every code-60 investigation is shortened by this one line. curl prints the bundle it was compiled to use in its verbose handshake output:

Print the CA bundle path
curl -v https://example.com 2>&1 | grep -iE "CAfile|CApath"
Expected output
*  CAfile: /etc/ssl/certs/ca-certificates.crt
*  CApath: /etc/ssl/certs

Three things override that path, in increasing order of precision:

export CURL_CA_BUNDLE=/path/to/ca-bundle.crt   # every curl invocation in the shell
curl --cacert /path/to/root-ca.crt https://example.com   # this request only
curl --capath /etc/ssl/certs https://example.com         # a directory of hashed certs

unable to get local issuer certificate #

The most common of the four. curl could not find the CA that signed the certificate it was handed. Ask the server what it sends and count the certificates:

How many certificates does the server offer?
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null \
| grep -c "BEGIN CERTIFICATE"

One certificate from a public CA means the server is misconfigured — it is not sending its intermediates — and every non-browser client will fail against it. Two or more means the chain is fine and the root is missing from curl’s bundle, which is normal for a private or corporate CA.

The full diagnosis and both fixes are on unable to get local issuer certificate. To make curl accept a private root without touching the system bundle:

curl --cacert /etc/ssl/certs/corp-root-ca.crt https://internal.example.com

Self-signed certificate #

The chain terminates in a certificate that signed itself, so there is no CA to appeal to. Confirm by comparing subject and issuer — on a self-signed certificate they are identical:

Subject and issuer of a self-signed certificate
openssl x509 -in self-signed.crt -noout -subject -issuer
Expected output
subject=CN=localhost
issuer=CN=localhost

For development and internal services this is expected, and the correct response is to trust that specific certificate rather than to stop verifying:

curl --cacert self-signed.crt https://localhost:8443

For anything reachable from the internet, a self-signed certificate means no client will trust it by default. Get a free certificate from a public CA instead.

Hostname does not match #

The certificate is valid, correctly signed and in date — it was simply issued for a different name. curl checks the Subject Alternative Name extension, and since RFC 6125 the Common Name is not consulted at all by modern clients. List the names the certificate is actually valid for:

Which names is this certificate valid for?
openssl x509 -in certificate.crt -noout -ext subjectAltName
Expected output
X509v3 Subject Alternative Name:
  DNS:example.com, DNS:www.example.com

If the name you requested is absent, the certificate needs reissuing with the right SAN entries. A certificate carrying only a Common Name and no SAN extension fails this check against every current client, however correct the CN looks.

Fixing it for libcurl consumers #

curl is a library long before it is a command, so the same failure surfaces under other names:

Consumer How to point it at the right bundle
git git config --global http.sslCAInfo /path/to/ca-bundle.crt
PHP / Composer curl.cainfo and openssl.cafile in php.ini
Node (node-fetch, axios) NODE_EXTRA_CA_CERTS=/path/to/root-ca.crt
Python requests REQUESTS_CA_BUNDLE=/path/to/ca-bundle.crt
Any shell CURL_CA_BUNDLE=/path/to/ca-bundle.crt

What not to do #

Frequently asked questions

What is the difference between curl error 60 and error 77?
Error 60 means curl read its CA bundle and the certificate failed verification. Error 77, error setting certificate verify locations, means curl could not read the bundle file at all — the path is wrong or unreadable, so nothing was verified against anything.
Why does the browser load the site but curl refuses it?
Browsers repair incomplete chains. They cache intermediates from earlier visits and fetch missing ones over the Authority Information Access extension, and curl does neither. A site that only works in a browser is genuinely misconfigured, and other clients will hit the same wall.
Is curl -k safe for a script that runs on an internal network?
No. -k accepts any certificate from anything that answers on that address, so an internal network with a compromised host or a poisoned DNS entry is exactly where it does the most damage. Point curl at the internal CA with --cacert instead; it is one flag either way.
How do I fix this for git rather than curl?
git uses libcurl and shows the same message. Set the bundle with git config --global http.sslCAInfo /path/to/ca-bundle.crt. Do not use http.sslVerify false, which is git's equivalent of -k.
Why does the same curl command work on my machine but fail in Docker?
A slim base image often ships no CA bundle at all. apk add ca-certificates on Alpine or apt-get install -y ca-certificates on Debian installs one, and an image built FROM scratch needs the bundle copied in explicitly.

References