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 certificatecURL error 51: SSL: no alternative certificate subject name matches target host namecURL error 77: error setting certificate verify locations- Works on one server but fails on another
- Works with
curlcommand line but fails in PHP
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/dataCommon 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.cainfonot set in php.ini
Step-by-Step Fix
- 1.Download and configure CA bundle:
- 2.```bash
- 3.# Download the latest CA bundle
- 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.Set CA bundle in cURL code:
- 2.```php
- 3.// Without Guzzle
- 4.$ch = curl_init('https://api.example.com/data');
- 5.curl_setopt_array($ch, [
- 6.CURLOPT_RETURNTRANSFER => true,
- 7.CURLOPT_CAINFO => '/usr/local/share/ca-certificates/cacert.pem',
- 8.CURLOPT_SSL_VERIFYPEER => true, // Always true in production
- 9.CURLOPT_SSL_VERIFYHOST => 2,
- 10.CURLOPT_TIMEOUT => 30,
- 11.]);
- 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.Configure Guzzle with proper SSL settings:
- 2.```php
- 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.Handle corporate proxy certificate:
- 2.```php
- 3.// Add corporate CA to the CA bundle
- 4.$caBundle = file_get_contents('/usr/local/share/ca-certificates/cacert.pem');
- 5.$corporateCa = file_get_contents('/etc/ssl/certs/corporate-ca.crt');
- 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.NEVER disable verification in production:
- 2.```php
- 3.// DANGEROUS - enables man-in-the-middle attacks
- 4.// NEVER do this in production code!
- 5.curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
- 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.cainfoin php.ini for all PHP installations - Download updated CA bundle as part of server provisioning
- Use
openssl ca-certificatespackage 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_clientto diagnose certificate chain issues: - ```bash
- openssl s_client -connect api.example.com:443 -servername api.example.com
`