← Блог

Сертификат Минцифры: как убрать ошибку SSL

Коротко: если запрос к API падает с ошибкой unable to get local issuer certificate, self signed certificate in certificate chain, x509: certificate signed by unknown authority или PKIX path building failed, а в Яндекс Браузере адрес открывается, скорее всего сервер перешёл на сертификат НУЦ Минцифры. Его корню, Russian Trusted Root CA, curl, Python, Node.js и Java по умолчанию не доверяют. Отключать проверку не нужно: скачайте корень на gosuslugi.ru/crt и добавьте его туда, где его ищет ваш инструмент. В Debian, Ubuntu и Astra Linux хватает трёх команд:

curl -O https://gu-st.ru/content/lending/russian_trusted_root_ca_pem.crt
sudo cp russian_trusted_root_ca_pem.crt \
  /usr/local/share/ca-certificates/russian_trusted_root_ca.crt
sudo update-ca-certificates

Python, Node.js, Java и Docker-образы системное хранилище не читают, им корень передают отдельно.

К своему инструменту: Linux · macOS · Windows · curl · Python · Node.js · Java · Go · PHP · git · Docker · Postman и другие клиенты

Как выглядит ошибка в разных инструментах#

ИнструментЧто пишет
curlSSL certificate problem: unable to get local issuer certificate, код 60
Python (requests, httpx, urllib)[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate
Node.jsUNABLE_TO_GET_ISSUER_CERT_LOCALLY или SELF_SIGNED_CERT_IN_CHAIN
PostmanError: unable to get local issuer certificate или self signed certificate in certificate chain
JavaPKIX path building failed … unable to find valid certification path to requested target
Go и Tetivax509: certificate signed by unknown authority
PHPSSL certificate problem: … в curl, error:0A000086:SSL routines::certificate verify failed в потоках
git на Debianserver verification failed: certificate signer not trusted

У ошибки два варианта, и лечатся они одинаково. Если сервер присылает корень вместе с цепочкой, как sberbank.ru, OpenSSL пишет self-signed certificate in certificate chain. Если не присылает, как vtb.ru или platform-api2.max.ru, пишет unable to get local issuer certificate. curl с версии 8.17 формулирует иначе: SSL certificate OpenSSL verify result: unable to get local issuer certificate (20).

Почему ошибка появилась сейчас#

Российские компании переводят сайты и API на сертификаты Национального удостоверяющего центра. Летом 2026 года процесс ускорился: зарубежный удостоверяющий центр GlobalSign начал отзывать сертификаты российских организаций, в начале августа на сертификаты НУЦ перешли сайты крупных банков, а MAX с 19 июля попросил разработчиков слать запросы к Bot API на новый адрес и добавить сертификат Минцифры в доверенные. Тот же корень нужен для GigaChat API. Свои инструкции по переходу API опубликовали Т-Банк (сам портал открывается только с установленным корнем) и Сбер.

Браузеры ведут себя по-разному. В Яндекс Браузере корень НУЦ уже есть во внутреннем хранилище, системное он не меняет. Остальным браузерам нужен установленный сертификат. Поэтому типичная картина такая: в браузере всё открывается, а скрипт, сервер или CI на Linux падают.

Как убедиться, что дело в НУЦ#

Посмотрите, кто выдал сертификат сервера:

openssl s_client -connect platform-api2.max.ru:443 \
  -servername platform-api2.max.ru -showcerts </dev/null

В начале вывода будет цепочка. Если в ней есть Russian Trusted Sub CA и Russian Trusted Root CA, это сертификат НУЦ:

 0 s:CN=*.max.ru, O=MAX LLC, ...
   i:C=RU, O=The Ministry of Digital Development and Communications, CN=Russian Trusted Sub CA
 1 s:C=RU, O=The Ministry of Digital Development and Communications, CN=Russian Trusted Sub CA
   i:C=RU, O=The Ministry of Digital Development and Communications, CN=Russian Trusted Root CA
Verify return code: 20 (unable to get local issuer certificate)
Сервер отдаёт свой сертификат и промежуточный. Корень должен лежать у вас, а его нет

Сервер присылает свой сертификат и промежуточный Russian Trusted Sub CA, а корень по правилам TLS должен уже быть у клиента. Его нет, отсюда ошибка про «local issuer»: инструмент не нашёл у себя того, кто подписал промежуточный сертификат.

Если в цепочке нет Russian Trusted CA#

У той же ошибки есть и другие частые причины:

  • Цепочка обрывается на сертификате сервера (есть 0 s:, но нет 1 s:): сервер не присылает промежуточный. Браузеры часто это прощают, другие клиенты нет. Чинить нужно на сервере: в его файле сертификата должна быть вся цепочка.
  • Издатель ваша компания или средство защиты, например корпоративный прокси, который проверяет трафик: попросите корень у ИТ и поставьте его теми же шагами, что ниже.
  • Издатель известный публичный центр: у инструмента устарел набор корней. Обновите ca-certificates, certifi или саму среду выполнения.

Где скачать Russian Trusted Root CA#

Официальный источник один: страница gosuslugi.ru/crt. Для серверов и разработки нужен файл корневого сертификата Russian Trusted Root CA в формате PEM, обычно он называется russian_trusted_root_ca_pem.crt. Промежуточный ставить не нужно: сервер присылает его сам. К тому же промежуточных у НУЦ два, старый Russian Trusted Sub CA и новый 2024 года, а серверы сейчас присылают новый, так что старый файл промежуточного не поможет.

Для скриптов корень можно скачать с сервера Госуслуг напрямую:

curl -O https://gu-st.ru/content/lending/russian_trusted_root_ca_pem.crt

Если этот адрес сам отвечает ошибкой сертификата, скачайте файл в Яндекс Браузере и сверьте отпечаток.

Если файл .cer бинарный, то есть не начинается со строки -----BEGIN CERTIFICATE-----, это DER, переведите его в PEM:

openssl x509 -inform DER -in russian_trusted_root_ca.cer \
  -out russian_trusted_root_ca_pem.crt

И проверьте, что это тот самый корень. Он действует до 27 февраля 2032 года, его отпечаток SHA-256:

D2:6D:2D:02:31:B7:C3:9F:92:CC:73:85:12:BA:54:10:35:19:E4:40:5D:68:B5:BD:70:3E:97:88:CA:8E:CF:31
openssl x509 -in russian_trusted_root_ca_pem.crt -noout -fingerprint -sha256

Какое хранилище у вашего инструмента#

После установки сертификата в систему ошибка может остаться: многие среды носят свой набор доверенных корней и системный не читают.

Системное хранилище читают curl в Linux и Windows, Go, PowerShell, urllib в Python на Linux и Windows и Tetiva. Свой набор корней носят Python requests и httpx, Node.js и Java, а у каждого Docker-образа хранилище своё, внутри образа. Поэтому порядок такой: сначала корень в систему, потом отдельно для тех, кто системное хранилище не читает.

Кто смотрит в системное хранилище, а кто носит своё

Как установить сертификат Минцифры по шагам#

Linux

Debian, Ubuntu и Astra Linux:

sudo cp russian_trusted_root_ca_pem.crt \
  /usr/local/share/ca-certificates/russian_trusted_root_ca.crt
sudo update-ca-certificates

Файл должен быть в PEM и с расширением .crt, иначе update-ca-certificates его не возьмёт. В ответ команда напишет 1 added.

RHEL, Fedora и их родственники:

sudo cp russian_trusted_root_ca_pem.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust

В Alpine сначала нужен пакет ca-certificates: в базовом образе есть файл с корнями, но нет команды update-ca-certificates. В Arch корень добавляют командой sudo trust anchor --store russian_trusted_root_ca_pem.crt.

Если прав администратора нет (виртуальный хостинг, чужой CI), соберите свой файл из системного набора и корня Минцифры и передавайте его вместо системного:

cat /etc/ssl/certs/ca-certificates.crt russian_trusted_root_ca_pem.crt > ~/ca-bundle.pem

В RHEL системный набор лежит в /etc/pki/tls/certs/ca-bundle.crt. Такой файл подходит для --cacert в curl, REQUESTS_CA_BUNDLE в requests, SSL_CERT_FILE в httpx и curl.cainfo в PHP.

macOS

sudo security add-trusted-cert -d -r trustRoot \
  -k /Library/Keychains/System.keychain russian_trusted_root_ca_pem.crt

macOS попросит подтвердить изменение доверия в окне, поэтому по SSH и в CI эта команда не сработает. Системный /usr/bin/curl после этого корень видит: он проверяет сертификаты и по связке ключей. curl из Homebrew в свежих сборках тоже проверяет через систему, а старые берут свой файл сертификатов, и с ними надёжнее --cacert.

Windows

certutil -addstore -f Root russian_trusted_root_ca.cer

Команду запускают от администратора. Без прав добавьте ключ -user, тогда корень станет доверенным только для вашей учётной записи. certutil принимает файл с Госуслуг как есть, в PEM или DER. После этого корень видят curl.exe, PowerShell, Go и браузеры на Chromium.

curl на один запуск

curl --cacert russian_trusted_root_ca_pem.crt https://platform-api2.max.ru/me

То же делает переменная CURL_CA_BUNDLE. curl.exe в Windows по умолчанию проверяет по системному хранилищу и эту переменную не читает. --cacert он понимает, но тогда проверяет только по этому файлу, так что проще поставить корень в систему.

Python

requests и httpx проверяют сертификаты по пакету certifi и системное хранилище не читают. Варианты:

  • requests: переменная REQUESTS_CA_BUNDLE или параметр verify=. Она заменяет набор корней, а не дополняет его. Файл с одним корнем Минцифры оставит requests без всех остальных доверенных корней. Поэтому сначала добавьте корень в систему, а потом укажите системный файл целиком: REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt.
  • httpx: переменная SSL_CERT_FILE с тем же системным файлом или контекст ssl.create_default_context(cafile=...) в параметре verify.
  • Системное хранилище для всего приложения: пакет truststore, Python 3.10 и новее. Две строки при запуске приложения, и requests с httpx начнут проверять сертификаты через macOS, Windows или OpenSSL в Linux:
import truststore
truststore.inject_into_ssl()

Стандартный urllib читает системное хранилище в Linux и Windows. На macOS Python с python.org или из Homebrew в связку ключей не смотрит, там нужен truststore или SSL_CERT_FILE. pip с версии 24.2 на Python 3.10 и новее берёт системное хранилище сам.

Node.js

Node.js носит встроенный набор корней. Переменная NODE_EXTRA_CA_CERTS дополняет его и читается при старте процесса:

NODE_EXTRA_CA_CERTS=/path/russian_trusted_root_ca_pem.crt node app.js

Если корень уже в системе, можно запустить node --use-system-ca app.js. Флаг работает на всех трёх системах в Node.js 22.15 и новее в ветке 22, в 23.9 и новее и в 24.

Java

Корень добавляют в общее хранилище cacerts, пароль у него по умолчанию changeit. Ключ -cacerts есть начиная с JDK 9:

sudo keytool -importcert -cacerts -noprompt \
  -alias russian_trusted_root_ca \
  -file russian_trusted_root_ca_pem.crt -storepass changeit

Отдельный truststore через -Djavax.net.ssl.trustStore заменяет cacerts целиком: если положить в него только корень Минцифры, других доверенных корней в нём не будет. Поэтому сначала скопируйте cacerts и добавьте корень в копию. В Windows Java может взять системное хранилище: -Djavax.net.ssl.trustStoreType=Windows-ROOT.

Go

Go проверяет сертификаты по системе: в Linux читает файл с корнями и каталоги /etc/ssl/certs, в macOS и Windows спрашивает саму систему. Поэтому обычно хватает корня в системе. С Go 1.27 (для модулей с go 1.27 в go.mod) переменная SSL_CERT_FILE в macOS и Windows отключает системную проверку, и файл с одним корнем в ней ломает проверку для всех остальных сайтов. Вернуть старое поведение можно через GODEBUG=x509sslcertoverrideplatform=0. В коде корень можно добавить к системному пулу через x509.SystemCertPool() и AppendCertsFromPEM.

PHP

PHP берёт хранилище OpenSSL, и корня в системе обычно достаточно. Если файл задают явно в php.ini, через curl.cainfo и openssl.cafile, в нём должен быть полный набор корней, а не один корень Минцифры.

git

Для одного сервера корень удобнее подключить только к нему:

git config --global http.https://git.example.ru/.sslCAInfo \
  /path/russian_trusted_root_ca_pem.crt

Git для Windows по умолчанию проверяет сертификаты через системное хранилище, ему хватает certutil.

Docker

У образа своё хранилище, и корень с машины туда не попадает. В Dockerfile на Debian:

FROM debian:trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
  && rm -rf /var/lib/apt/lists/*
COPY russian_trusted_root_ca_pem.crt /usr/local/share/ca-certificates/russian_trusted_root_ca.crt
RUN update-ca-certificates

В slim-образах пакета ca-certificates нет, его ставят отдельно. Python и Node.js в контейнере всё равно не увидят системное хранилище, им нужны настройки из разделов выше.

Ловушка для тех, кто работает на Mac с OrbStack: он сам подкладывает в контейнеры сертификаты, которым доверяет macOS. Локально в Docker всё работает, а в CI тот же образ падает.

Если внутри Node.js, как у n8n, пересобирать образ не обязательно: смонтируйте файл и задайте переменную в docker-compose.yml:

services:
  n8n:
    image: n8nio/n8n
    environment:
      NODE_EXTRA_CA_CERTS: /certs/russian_trusted_root_ca_pem.crt
    volumes:
      - ./russian_trusted_root_ca_pem.crt:/certs/russian_trusted_root_ca_pem.crt:ro

Если с ошибкой x509: certificate signed by unknown authority падает сам docker pull или docker login, корень нужен не образу, а демону Docker: поставьте его в систему и перезапустите Docker (sudo systemctl restart docker) или положите файл в /etc/docker/certs.d/<адрес реестра>/ca.crt.

Postman, Insomnia, Bruno и Tetiva

  • Postman: Settings → вкладка Certificates → переключатель «CA certificates», выбрать PEM-файл.
  • Insomnia: с конца 2023 года берёт системное хранилище. Свой корень можно добавить для коллекции в окне «Manage Certificates», блок «CA Certificate». Клиент будет использовать этот файл вместо системного хранилища, поэтому в нём нужен полный набор корней.
  • Bruno: с версии 2.11 тоже доверяет системному хранилищу. Свой файл: Preferences → General → «Use Custom CA Certificate».
  • Hoppscotch Desktop: Settings → Interceptors → Native → «CA Certificates».
  • Tetiva: проверяет сертификаты по системному хранилищу, своего списка корней и переключателя «не проверять» в клиенте нет. Корень, установленный в систему по шагам выше, Tetiva видит сразу, а в Linux после перезапуска. Пока корня нет, запрос заканчивается такой ошибкой:
Запрос к серверу на сертификате НУЦ в Tetiva на Linux или Windows, пока в системе нет корня

На macOS текст другой: x509: “*.max.ru” certificate is not trusted.

Почему не -k и не verify=False#

Флаги -k в curl, verify=False в Python и NODE_TLS_REJECT_UNAUTHORIZED=0 в Node.js отключают проверку целиком, а не только для НУЦ. С ними клиент примет сертификат от кого угодно, в том числе от того, кто перехватил трафик. Такой флаг легко оставить «временно» и забыть, особенно в CI. С добавленным корнем проверку можно оставить включённой, и она продолжит защищать запросы ко всем остальным сайтам.

Ещё одна ловушка: корень Минцифры, добавленный в общее системное хранилище, начинает действовать для всех сайтов, а не только для нужного API. Если это не подходит, держите отдельный файл с корнем и передавайте его только тем запросам, которым он нужен: --cacert в curl, verify= в Python, отдельный truststore в Java.

Шпаргалка#

  • Признак: в цепочке openssl s_client есть Russian Trusted Sub CA.
  • Корень: Russian Trusted Root CA с gosuslugi.ru/crt, SHA-256 начинается с D2:6D:2D:02.
  • curl на один запуск: --cacert russian_trusted_root_ca_pem.crt.
  • Linux: /usr/local/share/ca-certificates/ и update-ca-certificates или /etc/pki/ca-trust/source/anchors/ и update-ca-trust.
  • Python: REQUESTS_CA_BUNDLE или verify=. Node.js: NODE_EXTRA_CA_CERTS. Java: keytool -importcert.
  • Не делать: -k, verify=False, NODE_TLS_REJECT_UNAUTHORIZED=0.