Introduction

PHP cURL verifies SSL certificates by default since PHP 7.0. When connecting to HTTPS endpoints, cURL checks the server certificate against a CA bundle file. If the CA bundle is outdated, missing the signing authority, or the server uses a self-signed certificate, cURL refuses the connection with an SSL certificate error. Disabling verification with CURLOPT_SSL_VERIFYPEER is a security risk that enables man-in-the-middle attacks.

Symptoms

  • cURL error 60: SSL certificate problem: unable to get local issuer certificate
  • cURL error 51: SSL: no alternative certificate subject name matches target host name
  • cURL error 77: error setting certificate verify locations
  • Works on one server but fails on another
  • Works with curl command line but fails in PHP
bash
PHP Fatal error:  Uncaught GuzzleHttp\Exception\RequestException:
cURL error 60: SSL certificate problem: unable to get local issuer certificate
(see https://curl.se/libcurl/c/libcurl-errors.html)
for https://api.example.com/v1/data

Common Causes

  • PHP not configured with a CA bundle path
  • CA bundle outdated (missing newer CAs like Let's Encrypt R3)
  • Corporate MITM proxy using internal CA
  • Server certificate SAN does not match the hostname
  • curl.cainfo not set in php.ini

Step-by-Step Fix

  1. 1.Download and configure CA bundle:
  2. 2.```bash
  3. 3.# Download the latest CA bundle
  4. 4.curl -o /usr/local/share/ca-certificates/cacert.pem https://curl.se/ca/cacert.pem

# Configure in php.ini # /etc/php/8.2/fpm/php.ini curl.cainfo = /usr/local/share/ca-certificates/cacert.pem openssl.cafile = /usr/local/share/ca-certificates/cacert.pem

# Restart PHP-FPM sudo systemctl restart php8.2-fpm ```

  1. 1.Set CA bundle in cURL code:
  2. 2.```php
  3. 3.// Without Guzzle
  4. 4.$ch = curl_init('https://api.example.com/data');
  5. 5.curl_setopt_array($ch, [
  6. 6.CURLOPT_RETURNTRANSFER => true,
  7. 7.CURLOPT_CAINFO => '/usr/local/share/ca-certificates/cacert.pem',
  8. 8.CURLOPT_SSL_VERIFYPEER => true, // Always true in production
  9. 9.CURLOPT_SSL_VERIFYHOST => 2,
  10. 10.CURLOPT_TIMEOUT => 30,
  11. 11.]);
  12. 12.$response = curl_exec($ch);

if (curl_errno($ch)) { $error = curl_error($ch); error_log("cURL error: $error"); throw new RuntimeException("API request failed: $error"); } curl_close($ch); ```

  1. 1.Configure Guzzle with proper SSL settings:
  2. 2.```php
  3. 3.use GuzzleHttp\Client;

$client = new Client([ 'base_uri' => 'https://api.example.com', 'verify' => '/usr/local/share/ca-certificates/cacert.pem', 'timeout' => 30, 'connect_timeout' => 10, ]);

$response = $client->get('/v1/data');

// For corporate environments with internal CA: $client = new Client([ 'base_uri' => 'https://api.example.com', 'verify' => [ '/usr/local/share/ca-certificates/cacert.pem', '/path/to/corporate-ca.crt', ], ]); ```

  1. 1.Handle corporate proxy certificate:
  2. 2.```php
  3. 3.// Add corporate CA to the CA bundle
  4. 4.$caBundle = file_get_contents('/usr/local/share/ca-certificates/cacert.pem');
  5. 5.$corporateCa = file_get_contents('/etc/ssl/certs/corporate-ca.crt');
  6. 6.file_put_contents('/usr/local/share/ca-certificates/combined-ca.pem', $caBundle . $corporateCa);

// Use combined bundle curl_setopt($ch, CURLOPT_CAINFO, '/usr/local/share/ca-certificates/combined-ca.pem'); ```

  1. 1.NEVER disable verification in production:
  2. 2.```php
  3. 3.// DANGEROUS - enables man-in-the-middle attacks
  4. 4.// NEVER do this in production code!
  5. 5.curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  6. 6.curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

// If you MUST test against a self-signed cert in development only: if (getenv('APP_ENV') === 'development') { curl_setopt($ch, CURLOPT_CAINFO, __DIR__ . '/dev-self-signed.crt'); } ```

Prevention

  • Set curl.cainfo in php.ini for all PHP installations
  • Download updated CA bundle as part of server provisioning
  • Use openssl ca-certificates package on Debian/Ubuntu:
  • ```bash
  • sudo apt install ca-certificates
  • sudo update-ca-certificates
  • `
  • Test HTTPS connectivity in CI pipeline
  • Monitor SSL certificate expiration of API endpoints
  • Use openssl s_client to diagnose certificate chain issues:
  • ```bash
  • openssl s_client -connect api.example.com:443 -servername api.example.com
  • `