# PHP "cURL error 60: SSL certificate problem": how to fix

> PHP cURL error 60 means libcurl could not verify the server certificate, usually because PHP has no CA bundle. Set curl.cainfo; keep verification on.

Source: https://ssl.sh/errors/php-curl-error-60/
Last updated: 2026-11-10

**`cURL error 60: SSL certificate problem` means PHP's curl extension reached the server but could not verify its certificate chain.** On Windows and hand-built PHP the usual reason is that PHP has no CA bundle configured; on Linux servers it is more often a missing intermediate certificate on the remote server or a private root that PHP does not know.

## Quick fix

Setting `curl.cainfo` to a current CA bundle fixes most cases.

Check which CA file PHP is using, from the same environment that fails (web or CLI):

```bash
php --ini
php -i | grep -iE 'curl.cainfo|openssl.cafile|openssl.capath'
php -r 'print_r(openssl_get_cert_locations());'
```

If `curl.cainfo` and `openssl.cafile` are empty and the default file does not exist, set them in the php.ini that PHP loads:

```ini
curl.cainfo = "/etc/ssl/certs/ca-certificates.crt"
openssl.cafile = "/etc/ssl/certs/ca-certificates.crt"
```

On Windows use the path of a `cacert.pem` downloaded from curl.se. Then restart PHP-FPM, Apache or IIS. If the bundle is already correct, test the remote server's chain with the [SSL checker](https://ssl.sh/tools/ssl-checker/).

## Causes

### 1. PHP has no CA bundle configured

Windows builds, XAMPP, WampServer and many custom builds ship libcurl without a usable CA path.

- **Confirm:** `openssl_get_cert_locations()` points to a file that does not exist, and `curl.cainfo` is empty.
- **Fix:** set `curl.cainfo` (used by the curl extension) and `openssl.cafile` (used by stream functions such as `file_get_contents('https://...')`) to a valid bundle.

### 2. The remote server sends an incomplete chain

If PHP fails for one site but works for others, the remote site may be missing its intermediate certificate.

- **Confirm:** `openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts` lists only one certificate.
- **Fix:** the server owner must install the full chain. See [missing intermediate certificate](https://ssl.sh/errors/missing-intermediate-certificate/) and the [chain builder](https://ssl.sh/tools/chain-builder/).

### 3. The CA bundle is outdated

An old `cacert.pem` or an unpatched OS does not contain newer roots, and fails for sites that chain to them.

- **Fix:** update the OS `ca-certificates` package, or download a fresh `cacert.pem`.

### 4. A corporate proxy or internal CA

TLS-inspecting proxies and internal APIs use roots that are not in Mozilla's list.

- **Fix:** append the PEM root to a copy of the bundle and point `curl.cainfo` at the copy, or pass it per request:

```php
$ch = curl_init('https://intranet.example.com/api');
curl_setopt($ch, CURLOPT_CAINFO, '/etc/ssl/certs/company-bundle.pem');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$body = curl_exec($ch);
```

### 5. The wrong php.ini was edited

CLI and FPM commonly use separate files (for example `/etc/php/8.3/cli/php.ini` and `/etc/php/8.3/fpm/php.ini` on Debian and Ubuntu).

- **Fix:** read "Loaded Configuration File" from `phpinfo()` on the web side and edit that file.

## Fixes by environment

Every fix gives PHP a current CA bundle through `curl.cainfo` or `openssl.cafile`, plus any private roots.

**Debian / Ubuntu:**

```bash
sudo apt-get install --reinstall ca-certificates
sudo systemctl restart php8.3-fpm   # match your PHP version
```

**RHEL family:** the bundle is `/etc/pki/tls/certs/ca-bundle.crt`; add private roots to `/etc/pki/ca-trust/source/anchors/` and run `update-ca-trust`.

**Windows, XAMPP, WampServer:** download `cacert.pem`, set both directives in the php.ini used by Apache, and restart Apache from the control panel.

**Docker:** make sure the image has `ca-certificates` installed, and copy private roots in before running `update-ca-certificates`.

**Frameworks:** Guzzle and Laravel accept `'verify' => '/path/to/bundle.pem'`; Symfony HttpClient accepts the `cafile` option. Composer has its own settings; see [Composer SSL operation failed](https://ssl.sh/errors/composer-ssl-operation-failed/).

**WordPress:** core HTTP requests use the CA bundle shipped in `wp-includes/certificates/`. Keeping WordPress updated keeps that bundle current; plugins that call curl directly use PHP's settings.

## Don't do this

- **`curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false)`.** This is the most copied "fix" for error 60 and disables chain verification, so any network attacker can impersonate the API.
- **`CURLOPT_SSL_VERIFYHOST` set to 0.** It disables the hostname check, so a valid certificate for any other domain is accepted.
- **`'verify' => false` in Guzzle or Laravel.** Same effect, often left in production after a quick test.
- **Using a `cacert.pem` from a random repository.** Use the file published by the curl project or your OS package.
