curl error 60 means curl connected to the server over TLS but could not verify the server's certificate against the trusted certificate authorities (CAs) on your machine, so it aborted before sending your request. Browsers, curl and libraries such as Python requests, PHP and Git all perform this check, and they report it in different words.
You see it when running curl https://..., in PHP's cURL extension as cURL error 60, in WordPress HTTP API failures, in Git as "SSL certificate problem", and in CI jobs and Docker builds. The text after "SSL certificate problem:" names the actual reason:
curl: (60) SSL certificate problem: unable to get local issuer certificate
curl: (60) SSL certificate problem: certificate has expired
curl: (60) SSL certificate problem: self-signed certificate
curl: (60) SSL certificate problem: self-signed certificate in certificate chain
curl: (60) SSL: no alternative certificate subject name matches target host name 'example.com'
Decide whose side is broken
The central question is whether the server is misconfigured (everyone is affected) or your machine is (only you). Test the same URL from another machine, or use the SSL Checker and SSL Chain Debugger, which report chain problems from outside. A browser that loads the site fine does not prove the server is right, because browsers fetch missing intermediates themselves and curl does not.
Causes, most likely first
- The server sends an incomplete chain. It presents only its own certificate, not the intermediate CA certificate. This is the classic cause of "unable to get local issuer certificate" and the server owner must fix it.
- Your CA bundle is old or missing, common on minimal containers, old OS versions and embedded systems.
- A corporate proxy or antivirus intercepts TLS and re-signs traffic with a private CA that curl does not trust.
- The certificate expired (or is not valid yet because your system clock is wrong).
- A self-signed or private-CA certificate on an internal service.
- Hostname mismatch, where the name you use is not in the certificate's subject alternative names.
Step 1: see what the server presents
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null | grep -E "^ *[0-9]+ s:|^ *i:|Verify return code"
Reading the output:
- Only one certificate shown (depth 0) with
Verify return code: 21 (unable to verify the first certificate)means the chain is incomplete. That is a server problem. Verify return code: 20 (unable to get local issuer certificate)can be either server or your trust store; test from a second machine.10 (certificate has expired)is exactly that; check dates withopenssl x509 -noout -dates.18or19indicates self-signed.
Fix when you own the server: serve the full chain
Use the full chain file from your CA, not only the leaf certificate. With Let's Encrypt, that is fullchain.pem:
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
Older Apache versions use a separate SSLCertificateChainFile for the intermediates. After reloading, re-run the openssl test and the SSL Chain Debugger. If the certificate expired, renew it; see how to renew an SSL certificate and automate it and Let's Encrypt renewal failures.
Fix when your machine is at fault
Update the CA bundle.
# Debian, Ubuntu
sudo apt-get update && sudo apt-get install --reinstall ca-certificates && sudo update-ca-certificates
# RHEL, Rocky, Fedora
sudo dnf reinstall ca-certificates && sudo update-ca-trust
# Alpine (Docker)
apk add --no-cache ca-certificates && update-ca-certificates
In Docker images, install ca-certificates in the Dockerfile. If the machine is very old, the root certificates it trusts may have expired; Let's Encrypt's 2021 root change broke older OpenSSL 1.0.x systems this way, so upgrade the OS or library.
Check the clock. A wrong date makes valid certificates look expired or not yet valid: date -u and timedatectl.
Corporate or antivirus interception. Get the company root CA certificate from IT and add it to the system store (copy to /usr/local/share/ca-certificates/ and run update-ca-certificates), or point curl at it with --cacert. Many Git, Node and Python tools use their own bundle variables, such as SSL_CERT_FILE, REQUESTS_CA_BUNDLE or NODE_EXTRA_CA_CERTS.
Internal CA or self-signed certificate.
curl --cacert /path/to/internal-ca.pem https://intranet.example.local/
Specify a bundle explicitly for testing: curl --cacert /etc/ssl/certs/ca-certificates.crt https://example.com/. For PHP, set curl.cainfo and openssl.cafile in php.ini.
Hostname mismatch
If the message mentions a subject name, compare the names the certificate covers with the host you request:
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -ext subjectAltName
Fix by requesting a covered name, or by issuing a certificate that includes the one you need.
How to verify
Run curl -sS -o /dev/null -w "%{http_code} verify=%{ssl_verify_result}\n" https://example.com/ and expect 200 verify=0. Confirm from outside with the SSL Checker, and check TLS versions with the TLS Version Checker if an old client still fails. Add an SSL expiry alert so certificates do not lapse again.
Next steps
- Test your certificate with the SSL Checker.
- Debug chain order with the SSL Chain Debugger.
- Read SSL and TLS certificate errors explained.
- See the browser versions of this problem: ERR_CERT_AUTHORITY_INVALID and ERR_CERT_DATE_INVALID.
FAQ
Why does the site open in my browser but fail in curl?
Browsers can download a missing intermediate certificate on their own, while curl uses only what the server sends plus your local CA bundle. A chain that works in a browser but fails in curl is an incomplete chain.
Is using curl -k acceptable?
Only for a quick local test. It removes protection against man-in-the-middle attacks, so do not use it in production scripts.
Does error 60 mean the certificate is expired?
Not necessarily. The text after the colon tells you: "unable to get local issuer certificate", "certificate has expired", or "self-signed certificate" each point to a different fix.
The same command works on another server. Why?
That server has a newer CA bundle, a correct clock or no interception proxy. Compare openssl version, date and the CA bundle between the machines.
Get notified when this fix updates
Plus occasional practical tips on uptime and monitoring, nothing else. Unsubscribe anytime.
This page is general information written by our editorial team and updated when documentation changes. Servers and providers differ, so test changes safely first. See our editorial policy and disclaimer.