Mensaje de error
Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate
También se muestra como
- requests.exceptions.SSLError: HTTPSConnectionPool(host='api.example.com', port=8443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate
El host, el puerto y la ruta de la URL se toman de tu solicitud. El motivo más interno varía («self-signed certificate in certificate chain», «Hostname mismatch…», «certificate has expired»), y la línea termina con una ubicación en el código fuente como «(_ssl.c:1082)')))».
requests / urllib3 (Python) — requests.exceptions.SSLError wrapping urllib3 MaxRetryError
Qué significa
requests pidió una conexión a urllib3, la verificación del certificado falló durante el handshake TLS y urllib3 se rindió (con el valor predeterminado de requests, sin reintentos, después del primer intento). La parte útil está entre paréntesis: aquí OpenSSL no encontró el emisor en el bundle de CA que usa requests, que de forma predeterminada es la copia de certifi de las raíces de Mozilla, no el almacén del sistema operativo.
Por qué ocurre
requests verifica con certifi, no con el almacén del sistema
frecuenteUna CA corporativa o privada instalada con update-ca-certificates, en el almacén de Windows o en el llavero de macOS se ignora: requests.certs.where() apunta al cacert.pem de certifi. Es exactamente lo que muestra la reproducción.
Un proxy que inspecciona TLS vuelve a firmar el tráfico
frecuenteLas solicitudes a API públicas fallan solo en la red de la empresa porque la raíz del proxy no está en certifi.
El servidor no envía su certificado intermedio
ocasionalSi el servidor de la API solo envía su certificado hoja, OpenSSL no puede llegar a una raíz en ningún bundle; los navegadores lo ocultan porque descargan los intermedios.
REQUESTS_CA_BUNDLE o CURL_CA_BUNDLE apunta a un bundle parcial
ocasionalUn bundle que solo contiene la raíz de la empresa hace que fallen todos los hosts públicos (y al revés); las variables sustituyen a certifi en lugar de añadirse a él.
Cómo solucionarlo
- 1.
Pasar el bundle correcto con verify=
verify acepta la ruta de un archivo de bundle de CA o de un directorio de CA indexado por hash; mantiene activadas las comprobaciones del nombre de host y de las fechas.
python import requests # A CA file (or a directory prepared with c_rehash) for this call requests.get("https://api.example.com/", verify="/etc/ssl/certs/ca-certificates.crt") - 2.
Establecer REQUESTS_CA_BUNDLE para el proceso
Si haces que apunte al bundle del sistema (después de instalar allí tu CA), todas las llamadas de requests de ese proceso confiarán en las mismas raíces que curl y las herramientas del sistema operativo.
bash # For every requests call in the process (also honoured: CURL_CA_BUNDLE) export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt # Debian/Ubuntu system bundle python3 app.py - 3.
Usar el almacén de confianza del sistema operativo
Llamar a truststore.inject_into_ssl() al arrancar permite que requests verifique a través del sistema operativo, de modo que las raíces que despliega el departamento de TI funcionan sin tener que mantener bundles.
python # pip install truststore==0.10.5 — requests then verifies with the OS trust store import truststore truststore.inject_into_ssl() import requests requests.get("https://api.example.com/") - 4.
Configurar una sesión (Session) una sola vez
En las aplicaciones, establece Session.verify en un bundle que contenga las raíces públicas más la raíz de la empresa, y reutiliza la sesión.
python import requests session = requests.Session() session.verify = "/etc/ssl/private-ca/corp-ca-bundle.pem" # public roots + company root session.get("https://api.example.com/")