Introduction
PHP's stream functions and cURL verify SSL certificates by default, but when the CA certificate bundle is not found, when connecting to servers with self-signed certificates, or when behind a corporate proxy that performs TLS interception, requests fail with SSL operation failed with code 1 and certificate verify failed. Disabling verification with verify_peer => false fixes the immediate error but exposes the application to man-in-the-middle attacks. The proper fix is to configure the correct CA bundle path or add the corporate CA to the trust store.
Symptoms
file_get_contents(): SSL operation failed with code 1. OpenSSL Error messages:
error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failedOr:
Warning: file_get_contents(): Failed to enable crypto
Warning: file_get_contents(https://api.example.com/data): Failed to open stream: operation failedCommon Causes
- CA bundle not found: openssl.cafile not configured in php.ini
- Self-signed certificates: Internal APIs without proper CA
- Corporate TLS interception: Proxy replaces server certificates
- Outdated CA bundle: System CA bundle missing new root certificates
- Windows missing CA bundle: PHP on Windows does not include CA bundle by default
- Certificate expired: Server certificate past its validity period
Step-by-Step Fix
Step 1: Configure CA bundle in php.ini
; php.ini
[openssl]
openssl.cafile=/etc/ssl/certs/ca-certificates.crt
; Or use certifi's bundle (cross-platform)
; openssl.cafile=/path/to/vendor/composer/ca-bundle/res/cacert.pem```bash # Find your system CA bundle php -r "print_r(openssl_get_cert_locations());" # Look for 'default_cert_file' in output
# Download latest CA bundle curl -o /etc/ssl/certs/ca-certificates.crt https://curl.se/ca/cacert.pem ```
Step 2: Configure SSL context for specific requests
```php function httpsGet(string $url): string|false { $context = stream_context_create([ 'ssl' => [ 'verify_peer' => true, 'verify_peer_name' => true, 'cafile' => '/etc/ssl/certs/ca-certificates.crt', // For corporate CA: // 'cafile' => '/path/to/corporate-ca.crt', 'capture_peer_cert' => true, ], ]);
return file_get_contents($url, false, $context); }
// For development only - NEVER in production function httpsGetDev(string $url): string|false { $context = stream_context_create([ 'ssl' => [ 'verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true, ], ]);
return file_get_contents($url, false, $context); } ```
Step 3: Debug SSL issues
```php function debugSslConnection(string $url): void { $context = stream_context_create([ 'ssl' => [ 'capture_peer_cert' => true, 'capture_peer_cert_chain' => true, ], ]);
$stream = @fopen($url, 'r', false, $context); if ($stream === false) { echo "Failed to open stream\n"; echo "OpenSSL errors:\n"; while ($msg = openssl_error_string()) { echo " $msg\n"; } return; }
$meta = stream_get_meta_data($stream); $cert = $meta['options']['ssl']['peer_certificate'] ?? null;
if ($cert) { $info = openssl_x509_parse($cert); echo "Certificate subject: " . $info['subject']['CN'] . "\n"; echo "Valid from: " . date('Y-m-d', $info['validFrom_time_t']) . "\n"; echo "Valid to: " . date('Y-m-d', $info['validTo_time_t']) . "\n"; }
fclose($stream); } ```
Prevention
- Configure openssl.cafile in php.ini pointing to a current CA bundle
- Use composer/ca-bundle for cross-platform CA bundle resolution
- Never use verify_peer => false in production code
- For corporate environments, add the corporate CA to the system trust store
- Monitor SSL certificate expiry dates to prevent outages from expired certs
- Test SSL connectivity as part of deployment health checks
- Use openssl_error_string() to get detailed SSL error information