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.
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:
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:
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:
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:
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.
-kaccepts 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--cacertinstead; 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 usehttp.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-certificateson Alpine orapt-get install -y ca-certificateson Debian installs one, and an image builtFROM scratchneeds the bundle copied in explicitly.
References
- curl error codes — CURLE_PEER_FAILED_VERIFICATION, the code 60 maps to
- curl: SSL certificate verification — how curl selects a CA bundle and what --cacert overrides
- RFC 6125: identifying application services in PKIX certificates — the hostname matching rules behind the subject-name variant