cURL error 60: SSL certificate problem: self signed certificate in certificate chain in WordPress

Switchboard used for musical production with the mute button lit up.
How to mute the SSL certificate problem

Let's say you're developing locally using something like Valet or are running WordPress on an internal, corporate network. If you try to update WordPress, a plugin, or a theme and see this error message, it could mean that something (like Netskope) is injecting their own SSL certificates into your request.

You can confirm whether that is your issue by going to wordpress.org and inspecting the SSL certificate in your browser.

If you're using Brave (or Chrome) as your web browser, the steps are as follows:

  • Click on the lock to the left of the URL
  • Click on Connection is secure
  • Click on Certificate is valid
  • Look at who issued the certificate

It should look something like this:

Screenshot of WordPress.org's SSL certificate
SSL certificate issued by: Sectigo Limited

But if you're seeing this:

Screenshot of Netskope's SSL certificate
SSL certificate issued by: goscope.com

Then this could very well be your problem, because WordPress does not have Netskope listed in wp-includes/certificates/ca-bundle.crt as a valid certificate authority.

You have two options for resolving this.

Option 1. Disable SSL verification

The easiest way to circumvent this problem is to disable SSL verification for local websites running on your machine. Note that you should only do this for local development environments; not in production.

Create a new file called disable-ssl-verification.php in wp-content/mu-plugins.

Then paste the following code into it:

<?php

/**
 * Plugin Name: Disable SSL Verification
 * Description: WordPress themes and plugins can't be updated when using a self-signed SSL certificate. This plugin disables SSL verification if the WordPress site is loaded using a <code>.test</code> domain.
 * Version: 1.0.0
 * Author: Ryan Sechrest
 */

if (!isset($_SERVER['SERVER_NAME'])) {
    return;
}
if (!str_ends_with($_SERVER['SERVER_NAME'], '.test')) {
    return;
}
add_filter('https_ssl_verify', '__return_false');
  • We're checking to see if there is a SERVER_NAME set, e.g. example.org.test
  • Then we're checking to see if the server name ends with .test
  • If both conditions are true, then we return false to the https_ssl_verify WordPress filter to disable SSL verification

Now try to perform your update in WordPress again and it should work.

Option 2. Use custom certificate

If your WordPress instance is running on an internal network, you likely don't want to disable SSL verification. What you could do instead is supply WordPress with your self-signed certificate.

Open the certificate again like earlier, but this time click on the Details tab. At the very bottom click on the Export button and save the file as something like organization.crt in your mu-plugins folder:

Screenshot of Details tab of Certificate Viewer with Export button at bottom
Click on Export button at bottom

Now update the plugin as follows:

<?php

/**
 * Plugin Name: Use Organization's SSL Certificate
 * Description: WordPress themes and plugins can't be updated when using a self-signed SSL certificate. This plugin supplies organization's custom certificate to WordPress.
 * Version: 1.0.0
 * Author: Ryan Sechrest
 */

add_filter('https_ssl_verify', function($verify, $url) {
    if (!str_starts_with($url, 'https://api.wordpress.org')) {
        return $verify;
    }
    return WPMU_PLUGIN_DIR . '/organization.crt';
}, 10, 2);
  • We're only using the custom certificate if the request URL start with https://api.wordpress.org so that loopback requests aren't affected.
  • We return the path to the certificate we saved above.

Now try to update WordPress, a plugin, or a theme.

As an aside, if you're not familiar with mu-plugins, it's similar to a plugin, but it doesn't have to be activated in the WordPress admin. mu-plugins are always active if they exist. You can see them in the WordPress admin by going to Plugins and then filtering by Must-Use.

If you have any problems or need help tweaking this code snippet, leave a comment below.

Featured image by Mike Baumeister.