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

bash
file_get_contents(): SSL operation failed with code 1. OpenSSL Error messages:
error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed

Or:

bash
Warning: file_get_contents(): Failed to enable crypto
Warning: file_get_contents(https://api.example.com/data): Failed to open stream: operation failed

Common 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

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