Сообщение об ошибке
Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate
Также выводится как
- 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
Хост, порт и путь URL берутся из вашего запроса. Самая внутренняя причина бывает разной («self-signed certificate in certificate chain», «Hostname mismatch…», «certificate has expired»), а в конце строки стоит место в исходном коде, например «(_ssl.c:1082)')))».
requests / urllib3 (Python) — requests.exceptions.SSLError wrapping urllib3 MaxRetryError
Что это означает
Библиотека requests запросила у urllib3 соединение, при TLS-рукопожатии сертификат не прошёл проверку, и urllib3 сдалась (по умолчанию requests не делает повторных попыток, так что сразу после первой). Полезная часть — внутри скобок: здесь OpenSSL не нашёл издателя в пакете сертификатов CA, который использует requests, а по умолчанию это копия корневых сертификатов Mozilla из certifi, а не хранилище операционной системы.
Почему это происходит
requests проверяет сертификаты по certifi, а не по системному хранилищу
частоКорпоративный или частный CA, установленный через update-ca-certificates, в хранилище Windows или в связку ключей macOS, игнорируется: requests.certs.where() указывает на cacert.pem из certifi. Именно это и показывает пример воспроизведения.
Прокси с инспекцией TLS переподписывает трафик
частоЗапросы к публичным API завершаются ошибкой только в корпоративной сети, потому что корневого сертификата прокси нет в certifi.
Сервер не передаёт свой промежуточный сертификат
иногдаЕсли сервер API отправляет только конечный сертификат, OpenSSL не может дойти до корневого сертификата ни в одном пакете сертификатов; браузеры скрывают проблему, сами загружая промежуточные сертификаты.
REQUESTS_CA_BUNDLE или CURL_CA_BUNDLE указывает на неполный пакет сертификатов
иногдаПакет сертификатов только с корневым сертификатом компании приводит к ошибкам на всех публичных хостах (и наоборот); эти переменные заменяют certifi, а не дополняют его.
Как исправить
- 1.
Передайте нужный пакет сертификатов через verify=
verify принимает путь к файлу пакета сертификатов CA или к каталогу CA с хешированными именами файлов; проверки имени хоста и срока действия остаются включёнными.
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.
Задайте REQUESTS_CA_BUNDLE для процесса
Если указать в этой переменной системный пакет сертификатов (предварительно установив туда ваш CA), каждый вызов requests в этом процессе будет доверять тем же корневым сертификатам, что curl и инструменты ОС.
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.
Используйте хранилище доверенных сертификатов операционной системы
Вызов truststore.inject_into_ssl() при запуске позволяет requests проверять сертификаты средствами ОС, поэтому корневые сертификаты, развёрнутые ИТ-отделом, работают без сопровождения отдельных пакетов сертификатов.
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.
Настройте сессию (Session) один раз
В приложениях укажите в Session.verify пакет сертификатов с публичными корневыми сертификатами и корневым сертификатом компании и используйте эту сессию повторно.
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/")