certificate verify failed переводится как «проверка сертификата не прошла», а unable to get local issuer certificate — «не удалось найти у себя сертификат издателя». Программа подключилась к сайту по HTTPS, сервер показал сертификат, а клиент не смог подтвердить, что сертификат настоящий. Соединение обрывается до того, как уходят какие-либо данные: так HTTPS защищает от подмены сервера.
Сертификат проходит три проверки. Издатель должен быть в хранилище доверенных у клиента, срок действия не истёк, имя в сертификате совпадает с адресом. Какая из проверок не прошла, видно по хвосту сообщения:
| Сообщение | Что не так |
|---|---|
unable to get local issuer certificate | издателя нет в хранилище клиента: свой центр сертификации компании, прокси с проверкой трафика, сертификат Минцифры |
self-signed certificate | сертификат подписан сам собой, никакой центр за него не ручается |
certificate has expired | срок действия закончился — или на компьютере неправильные часы |
Hostname mismatch, no alternative certificate subject name matches | сертификат выписан на другое имя: обращаются по IP или по другому домену |
server certificate verification failed. CAfile: none | так те же проблемы описывает git в Debian и Ubuntu |
unable to verify the first certificate | так пишет Node.js: издатель неизвестен или сервер не прислал промежуточный сертификат |
Ниже — как посмотреть сертификат сервера и как правильно исправить ошибку в curl, Python, git, pip и Node.js. Вывод снят на стенде: сервер api.local с сертификатом от собственного центра сертификации «Koddo Test Root CA» и клиент на Debian 12.
Три проверки сертификата
Посмотри на сертификат сервера
Прежде чем что-то чинить, узнай, кому выдан сертификат, кто его подписал и до какого числа он действует:
openssl s_client -connect api.local:443 -servername api.local </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -enddate
subject=CN = api.local
issuer=CN = Koddo Test Root CA
notAfter=Oct 27 10:27:09 2027 GMT
Имя совпадает, срок в порядке, а издатель — центр сертификации, о котором клиент ничего не знает. Значит, нужно добавить его корневой сертификат в доверенные. Его выдаёт тот, кому принадлежит сервер: администратор компании, владелец API, отдел безопасности. На стенде это файл koddo-test-ca.crt.
curl: (60) SSL certificate problem
curl пишет причину после двоеточия:
curl https://api.local/status.json
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
Для разовой проверки передай корневой сертификат ключом --cacert:
curl --cacert koddo-test-ca.crt https://api.local/status.json
{"status": "ok"}
Другие причины выглядят так. Самоподписанный сертификат:
curl: (60) SSL certificate problem: self-signed certificate
Сертификат с истёкшим сроком — даже если издатель известен:
curl: (60) SSL certificate problem: certificate has expired
Обращение по IP, хотя сертификат выписан на имя api.local:
curl: (60) SSL: no alternative certificate subject name matches target host name '172.17.0.6'
Последние две ошибки сертификатом в доверенных не лечатся. Просроченный сертификат нужно перевыпустить на сервере, а обращаться — по тому имени, на которое он выписан.
Python: CERTIFICATE_VERIFY_FAILED
В requests та же ошибка спрятана в конец длинной цепочки:
python3 -c "import requests; requests.get('https://api.local/status.json')" 2>&1 | tail -1
requests.exceptions.SSLError: HTTPSConnectionPool(host='api.local', port=443): Max retries exceeded with url: /status.json (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:992)')))
Стандартный urllib пишет то же самое короче:
urllib.error.URLError: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:992)>
Для одного запроса передай путь к корневому сертификату в verify:
import requests
print(requests.get('https://api.local/status.json', verify='koddo-test-ca.crt').json())
{'status': 'ok'}
Самоподписанный и просроченный сертификаты Python описывает теми же словами, что и curl: certificate verify failed: self-signed certificate и certificate verify failed: certificate has expired.
Добавить сертификат в систему
Чтобы не передавать файл каждой программе, корневой сертификат добавляют в системное хранилище. В Debian и Ubuntu это делается от root:
sudo cp koddo-test-ca.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates
Updating certificates in /etc/ssl/certs...
rehash: warning: skipping ca-certificates.crt,it does not contain exactly one certificate or CRL
1 added, 0 removed; done.
Running hooks in /etc/ca-certificates/update.d...
done.
Файл должен иметь расширение .crt, предупреждение про ca-certificates.crt безвредно. После этого без всяких ключей работают curl, git и Python из пакетов системы:
curl https://api.local/status.json
python3 -c "import requests; print(requests.get('https://api.local/status.json').json())"
{"status": "ok"}
{'status': 'ok'}
В Windows сертификат добавляют в «Доверенные корневые центры сертификации» через оснастку certmgr.msc, в macOS — в связку ключей «Система» в программе «Связка ключей».
Ловушка: в виртуальном окружении ошибка остаётся
requests, установленный через pip, не смотрит в системное хранилище: у него свой список корневых сертификатов из пакета certifi. Поэтому в виртуальном окружении ошибка никуда не делась:
.venv/bin/python -c "import requests, certifi; print(requests.__version__, certifi.where())"
.venv/bin/python -c "import requests; requests.get('https://api.local/status.json')" 2>&1 | tail -1
2.34.2 /home/dev/.venv/lib/python3.11/site-packages/certifi/cacert.pem
requests.exceptions.SSLError: HTTPSConnectionPool(host='api.local', port=443): Max retries exceeded with url: /status.json (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:992)')))
Два способа направить его к системному хранилищу. Переменная окружения REQUESTS_CA_BUNDLE с путём к общему файлу сертификатов:
REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt .venv/bin/python -c "import requests; print(requests.get('https://api.local/status.json').json())"
{'status': 'ok'}
Или пакет truststore (pip install truststore): он подключает системное хранилище ко всему Python — и в Linux, и в Windows, и в macOS. Две строки в начале программы, до импорта requests:
import truststore
truststore.inject_into_ssl()
import requests
print(requests.get("https://api.local/status.json").json())
{'status': 'ok'}
git: server certificate verification failed
git в Debian и Ubuntu собран с GnuTLS и описывает ту же проблему по-своему. Пока сертификата нет в системном хранилище:
git ls-remote https://api.local/repo.git
fatal: unable to access 'https://api.local/repo.git/': server certificate verification failed. CAfile: none CRLfile: none
git, собранный с OpenSSL, пишет как curl: SSL certificate problem: unable to get local issuer certificate. Помогает сертификат в системном хранилище, а если его нельзя туда положить — путь к нему в настройках git:
git config --global http.sslCAInfo ~/koddo-test-ca.crt && git ls-remote https://api.local/repo.git
36af205ce8ee3a9bbf60198da47f1b5e25235b7f HEAD
36af205ce8ee3a9bbf60198da47f1b5e25235b7f refs/heads/main
Если сертификат нужен только для одного сервера, настройку можно ограничить его адресом: git config --global http.https://api.local/.sslCAInfo ~/koddo-test-ca.crt.
pip: ошибка SSL при установке пакетов
Через прокси компании с проверкой трафика pip не может проверить сертификат PyPI и повторяет попытки, пока не сдастся. Корневой сертификат прокси передают ключом --cert или сохраняют в настройках pip:
pip install --cert koddo-test-ca.crt requests
pip config set global.cert ~/koddo-test-ca.crt
pip, который входит в Debian и Ubuntu, уже берёт системное хранилище, а свежие версии pip умеют подключать его через truststore.
Node.js и npm
fetch в Node.js выбрасывает общее fetch failed, а причина лежит в cause:
const res = await fetch('https://api.local/status.json')
console.log(await res.json())
TypeError: fetch failed
at node:internal/deps/undici/undici:15456:13
at process.processTicksAndRejections (node:internal/process/task_queues:103:5)
at async file:///home/node/app/status.mjs:1:13 {
[cause]: Error: unable to verify the first certificate
at TLSSocket.onConnectSecure (node:internal/tls/wrap:1787:34)
at TLSSocket.emit (node:events:519:28)
at TLSSocket._finishInit (node:internal/tls/wrap:1176:8)
at ssl.onhandshakedone (node:internal/tls/wrap:962:12) {
code: 'UNABLE_TO_VERIFY_LEAF_SIGNATURE'
}
Node.js не читает системное хранилище по умолчанию, а дополнительные корневые сертификаты принимает из переменной NODE_EXTRA_CA_CERTS:
NODE_EXTRA_CA_CERTS=koddo-test-ca.crt node status.mjs
{ status: 'ok' }
У npm та же ошибка выглядит как UNABLE_TO_GET_ISSUER_CERT_LOCALLY или SELF_SIGNED_CERT_IN_CHAIN, а сертификат ему указывают командой npm config set cafile ~/koddo-test-ca.crt.
Сертификаты Минцифры
Часть российских сайтов работает на сертификатах национального удостоверяющего центра Минцифры. Его корневой сертификат, Russian Trusted Root CA, не входит ни в список Mozilla, на котором построены certifi и curl, ни в стандартные хранилища систем. Поэтому запрос к такому сайту из Python или curl заканчивается unable to get local issuer certificate, хотя в Яндекс Браузере сайт открывается. Корневой сертификат публикуется на Госуслугах (gosuslugi.ru/crt). Добавляй его в доверенные теми же способами, что выше, и только там, где программе действительно нужны такие сайты.
Почему не verify=False
Самый частый совет из интернета — выключить проверку: verify=False в Python, curl -k, NODE_TLS_REJECT_UNAUTHORIZED=0. Запрос пройдёт, а Python честно предупредит:
.venv/bin/python -c "import requests; print(requests.get('https://api.local/status.json', verify=False).json())"
/home/dev/.venv/lib/python3.11/site-packages/urllib3/connectionpool.py:1129: InsecureRequestWarning: Unverified HTTPS request is being made to host 'api.local'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
warnings.warn(
{'status': 'ok'}
Без проверки HTTPS шифрует трафик, но уже неважно, с кем: любой, кто встанет посередине — в общем Wi-Fi или на взломанном роутере, — подставит свой сертификат и прочитает пароли и токены. Для разовой проверки на своём стенде это допустимо. В коде, который уходит дальше твоего компьютера, — нет: добавь нужный корневой сертификат в доверенные.
Ошибки, на которых спотыкаются
В браузере сайт открывается, а curl и Python ругаются. Сервер не присылает промежуточный сертификат. Браузеры умеют докачать его сами, а curl, Python и Node.js — нет. Исправлять нужно на сервере: отдавать полную цепочку (в nginx — файл fullchain.pem вместо одного сертификата).
certificate has expired, хотя сертификат свежий. Проверь часы: date на компьютере с неправильной датой считает просроченным любой сертификат. Так бывает на виртуальных машинах после восстановления из снимка и на устройствах с севшей батарейкой часов.
Устаревший certifi. Список корневых сертификатов в certifi обновляется с новыми версиями пакета. Если в старом окружении перестали открываться сайты, которые работают в браузере, обнови его: pip install -U certifi.
Сертификат положили не туда. update-ca-certificates берёт только файлы с расширением .crt из /usr/local/share/ca-certificates/, а requests в виртуальном окружении системное хранилище вообще не видит — нужен REQUESTS_CA_BUNDLE или truststore.
Частые вопросы
Как перевести unable to get local issuer certificate
«Не удалось получить сертификат издателя из локального хранилища». Клиент не нашёл у себя центр сертификации, который подписал сертификат сервера, поэтому не может ему доверять.
Что значит SSL: CERTIFICATE_VERIFY_FAILED в Python
Python не смог проверить сертификат сервера. Причина написана сразу после certificate verify failed: — неизвестный издатель, самоподписанный сертификат, истёкший срок. Исправление зависит от причины: добавить корневой сертификат в доверенные, перевыпустить сертификат на сервере или поправить часы.