Mensaje de error
cURL error 60: SSL certificate problem: unable to get local issuer certificate
También se muestra como
- GuzzleHttp\Exception\ConnectException: 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:8443/
Guzzle lo formatea como «cURL error 60: SSL certificate problem: <reason> (see https://curl.se/libcurl/c/libcurl-errors.html) for <url>»; las versiones antiguas de Guzzle enlazaban https://curl.haxx.se/libcurl/c/libcurl-errors.html. El motivo que sigue a «problem:» varía («self-signed certificate in certificate chain», «certificate has expired»). Una llamada directa a curl_error() devuelve el texto que sigue a «cURL error 60: ».
PHP — Guzzle (and Laravel HTTP client, Symfony HttpClient on curl) over ext-curl; curl_errno() 60
Qué significa
La extensión curl de PHP (la que usan Guzzle, el cliente HTTP de Laravel y el transporte curl de Symfony HttpClient) no pudo verificar el certificado del servidor: error 60 de libcurl con el motivo de OpenSSL «unable to get local issuer certificate». libcurl no encontró en su bundle ninguna CA que hubiera emitido la cadena del servidor. En Windows, a menudo el bundle ni siquiera está configurado; en otros sistemas, la CA es privada o el servidor omite un certificado intermedio.
Por qué ocurre
PHP para Windows sin bundle de CA
frecuenteLas compilaciones de PHP para Windows no incluyen ningún bundle de CA; hasta que curl.cainfo apunte a uno (por ejemplo, el cacert.pem del proyecto curl), todas las solicitudes HTTPS fallan de esta forma.
Una CA privada o un proxy que inspecciona TLS
frecuenteLas API internas y las conexiones a través de un proxy tienen cadenas que terminan en raíces que no están en el bundle que usa libcurl.
El servidor omite su certificado intermedio
ocasionallibcurl con OpenSSL no descarga los certificados intermedios que faltan.
Imágenes de contenedor sin certificados de CA (actualizados)
ocasionalLas imágenes de PHP personalizadas construidas sin ca-certificates, o con un bundle antiguo, fallan también con hosts públicos.
Cómo solucionarlo
- 1.
Configurar curl.cainfo y openssl.cafile
Define ambos en php.ini para que ext-curl y los flujos (streams) de PHP usen el mismo bundle; recuerda que la CLI y PHP-FPM suelen cargar archivos php.ini distintos.
ini ; php.ini — CA bundle for ext-curl (Guzzle, Laravel Http, Symfony HttpClient) and for streams curl.cainfo = "/etc/ssl/certs/ca-certificates.crt" openssl.cafile = "/etc/ssl/certs/ca-certificates.crt" ; Windows: download cacert.pem from https://curl.se/docs/caextract.html and use its full path - 2.
Guzzle: pasar un bundle de CA a verify
verify acepta una ruta a un archivo de CA; Guzzle comprueba que el archivo existe y, si no, lanza «SSL CA bundle not found».
php // Trust a private CA for one client: verify takes a CA bundle path $client = new GuzzleHttp\Client(['verify' => '/etc/ssl/private-ca/corp-root-ca.pem']); $response = $client->get('https://api.example.com/'); - 3.
Instalar la CA en el almacén del sistema
En Linux, curl y OpenSSL de PHP usan de forma predeterminada el bundle de la distribución; openssl_get_cert_locations() muestra las rutas.
bash # Debian/Ubuntu-based PHP images: install the root, then check what PHP uses cp corp-root-ca.pem /usr/local/share/ca-certificates/corp-root-ca.crt && update-ca-certificates php -r 'var_dump(openssl_get_cert_locations()); echo ini_get("curl.cainfo"), PHP_EOL;' - 4.
Comprobar qué php.ini se está usando
php --ini enumera los archivos cargados; un ajuste añadido al archivo equivocado (CLI, FPM o Apache) no tiene ningún efecto.
bash php --ini # which php.ini files are loaded (CLI and FPM differ!) php -i | grep -E 'curl.cainfo|openssl.cafile'
Errores relacionados
cURL error 77: error setting certificate file: /etc/ssl/missing-ca.pemPHP ext-curl (Guzzle, Laravel HTTP client)SSL operation failed with code 1. OpenSSL Error messages:PHP streams (file_get_contents, fopen, stream_socket_client, SoapClient, mail libraries)curl: (60) SSL certificate problem: unable to get local issuer certificatecurl (OpenSSL backend)