cURL Error 60 in WordPress: Which Certificate Failed and Why
Error 60 means the remote server presented a certificate and WordPress refused to trust it. WordPress checks against its own bundled list of authorities, not the server's, which is why the browser can be happy while the site is not.
cURL error 60 means the connection reached the other server and WordPress declined to trust its certificate. It is not a timeout — that is cURL error 28 — and not a DNS or firewall problem. The server answered, the certificate was presented, and verification failed. The text after the number says why, and each variant has a different owner.
The detail WordPress hides from you
WordPress does not use your server's certificate store. Every request made through the HTTP API — update checks, package downloads, payment gateways, licence servers — is verified against the file WordPress ships at wp-includes/certificates/ca-bundle.crt. The WP_Http defaults set sslverify to true and sslcertificates to that path, and the same path is registered with the bundled Requests library. Requests to the site itself, such as the loopback request Site Health runs, are the exception: https_local_ssl_verify defaults to false.
Two consequences follow. Adding a certificate authority to the operating system (update-ca-certificates, a corporate root pushed by IT) changes nothing for WordPress. And curl https://example.com from the shell can succeed while WordPress fails, because the shell uses the system store. Any test that does not use the same bundle is testing something else.
Read the variant
The message is assembled as cURL error 60: plus cURL's own explanation:
| Message ends with | What it means | Whose problem |
|---|---|---|
unable to get local issuer certificate |
The chain does not lead to an authority in the bundle | Usually the remote server: missing intermediate |
self signed certificate / self-signed certificate in certificate chain |
A private or self-made authority | A dev server, or a proxy re-signing traffic |
certificate has expired |
Expired leaf or intermediate — or a wrong clock here | Remote server, or your server's time |
certificate subject name ... does not match target host name |
Certificate issued for a different name | Remote server, or DNS pointing somewhere unexpected |
Find the failing URL before anything else. The error usually surfaces as "Download failed." on an update screen, as a Site Health notice, or as a plugin's own error line in debug.log with debug logging on. The host in that URL is where you look next.
Test with WordPress's own bundle
Run the check from the web server, pointed at the same file WordPress uses:
curl -sv --cacert wp-includes/certificates/ca-bundle.crt https://api.example.com/ -o /dev/null
If this fails the same way, the reproduction is faithful. Then look at what the server actually sends:
openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts < /dev/null
A healthy response shows the leaf certificate followed by one or more intermediates. A single certificate with verify error:num=20:unable to get local issuer certificate is the classic missing-intermediate case: the server operator installed the certificate without its chain file. Browsers often paper over that; cURL never does.
Fixes by cause
Missing intermediate on someone else's server. Tell the operator; the fix is theirs (serve fullchain.pem, not cert.pem). If it is your own second site or API, fix the web server config the same way. Do not work around it on the WordPress side unless you control both ends.
Your own private authority. Internal APIs, staging servers and corporate proxies that inspect TLS all sign with a root nobody else trusts. Give WordPress a bundle that includes it — the https_ssl_verify filter accepts a path as well as a boolean:
add_filter( 'https_ssl_verify', function ( $verify, $url ) {
if ( str_starts_with( $url, 'https://intranet.example.com/' ) ) {
return WP_CONTENT_DIR . '/certs/internal-ca-bundle.crt';
}
return $verify;
}, 10, 2 );
Put it in a must-use plugin. Build internal-ca-bundle.crt by concatenating WordPress's bundle and your root, so the same file still trusts public sites. Scoping by URL keeps the change from affecting update downloads.
Proxy or security appliance re-signing traffic. Some hosts and office networks intercept outbound HTTPS and present their own certificate for every site. Every external request then fails with "self-signed certificate in certificate chain". The proper fix is an exemption for the server or the appliance's root added as above for all URLs — a decision for whoever runs the network.
Expired certificate, wrong clock. Check date -u on the server. A clock that has drifted days or years makes every certificate look expired or not yet valid, and every external request fails at once — which is the giveaway. Fix NTP. If only one host fails and the clock is right, its certificate really has expired.
Very old WordPress. An install years behind ships a bundle that predates current authorities and root rotations. Updating core refreshes it; if updates themselves fail, install the new release by uploading the files over SFTP.
Why update notices still appear
Plugin and core update checks retry over plain HTTP when the HTTPS request to WordPress.org fails, and raise an "An unexpected error occurred" warning instead of stopping. So the dashboard can list available updates while the download — normally HTTPS — fails with "Download failed. cURL error 60". The list proves nothing about TLS; the download is the real test. The other reasons a package download fails are covered in Download failed.
Common mistakes
- Turning off
sslverifyglobally throughhttp_request_args. It silences every symptom and leaves update packages unauthenticated. - Replacing
wp-includes/certificates/ca-bundle.crtwith a custom file. The next core update overwrites it and the problem returns with no trace of why it ever worked. - Adding the root to the operating system and expecting WordPress to pick it up.
- Testing with a browser or plain
curland declaring the remote server fine.
Verify
curl --cacert wp-includes/certificates/ca-bundle.crtagainst the failing host succeeds from the web server.- Tools › Site Health shows no HTTPS or loopback warnings.
- A plugin update or reinstall from Dashboard › Updates completes.
- The integration that first reported the error makes a successful call, and
debug.logstays quiet.
Frequently asked
- Browsers repair incomplete certificate chains by fetching or caching missing intermediates; cURL does not. And WordPress verifies against the ca-bundle.crt it ships in wp-includes/certificates, not against the browser's or the operating system's store. A chain that is missing an intermediate, or signed by a private authority, passes in the browser and fails here.
- Only on a local development site that talks to nothing real. On a live site it removes the check that stops a man-in-the-middle from feeding the site fake update packages or reading API keys, and it hides the actual defect, which is usually a misconfigured certificate on the remote server.
- Sometimes. Releases periodically ship a refreshed ca-bundle.crt, so a very old install can reject certificates from authorities added since. If WordPress is current, the bundle is not the problem and the fault is the remote server's chain, a proxy re-signing traffic, or the server clock.