← Blog

Russia's root CA and the local issuer certificate error

Short answer: if an API request fails with unable to get local issuer certificate, self signed certificate in certificate chain, x509: certificate signed by unknown authority or PKIX path building failed, the server may have moved to a certificate from Russia's national certificate authority (НУЦ Минцифры). curl, Python, Node.js and Java don't trust its root, Russian Trusted Root CA, by default. You don't need to switch verification off: download the root from gosuslugi.ru/crt and add it where your tool looks. On Debian and Ubuntu that's three commands:

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 and Docker images ignore the system store and need the root passed in separately.

Jump to your tool: Linux · macOS · Windows · curl · Python · Node.js · Java · Go · PHP · git · Docker · Postman and other clients

The same steps work for any root your tools don't know, such as a company's private CA, with that CA's root file.

What the error looks like in different tools#

ToolWhat it says
curlSSL certificate problem: unable to get local issuer certificate, code 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 or SELF_SIGNED_CERT_IN_CHAIN
PostmanError: unable to get local issuer certificate or self signed certificate in certificate chain
JavaPKIX path building failed … unable to find valid certification path to requested target
Go and Tetivax509: certificate signed by unknown authority
PHPSSL certificate problem: … in curl, error:0A000086:SSL routines::certificate verify failed in streams
git on Debianserver verification failed: certificate signer not trusted

The error comes in two variants, and the fix is the same. If the server sends the root along with the chain, as sberbank.ru does, OpenSSL says self-signed certificate in certificate chain. If it doesn't, as with vtb.ru or platform-api2.max.ru, it says unable to get local issuer certificate. From version 8.17 curl words it differently: SSL certificate OpenSSL verify result: unable to get local issuer certificate (20).

Why it's showing up now#

Russian companies are moving their sites and APIs to certificates from the national CA. The move sped up in summer 2026: GlobalSign started revoking certificates issued to Russian organisations, big banks switched their sites to the national CA in early August, and the MAX messenger asked bot developers to use a new API address from 19 July and to trust the national root. T-Bank (the portal itself opens only with the root installed) and Sber published their own migration guides. The same root is required for Sber's GigaChat API.

Browsers differ. Yandex Browser already trusts the national CA through its own certificate store, without changing the system store; other browsers need the certificate installed. So a site may open fine in your browser while requests from a script, a server or a Linux CI job fail.

How to tell it's the national CA#

Check who issued the server's certificate:

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

The chain comes first in the output. If it mentions Russian Trusted Sub CA and Russian Trusted Root CA, it's a national CA certificate:

 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)
The server sends its own certificate and the intermediate. The root should be on your side, and it isn't

The server sends its certificate and the Russian Trusted Sub CA intermediate; under TLS rules the root has to be on the client already. It isn't, hence the "local issuer" error: the tool couldn't find whoever signed the intermediate.

If the chain has no Russian Trusted CA#

The same error has other common causes:

  • The chain stops at the server certificate (0 s: and no 1 s:): the server doesn't send its intermediate. Browsers often paper over this, other clients don't. The fix is on the server: its certificate file has to include the chain.
  • The issuer is your company or a security product, such as a corporate proxy that inspects traffic: ask IT for the company root and use the steps below with that file.
  • The issuer is a well-known public CA: your tool's set of roots is outdated. Update ca-certificates, certifi or the runtime.

Where to download Russian Trusted Root CA#

The official source is gosuslugi.ru/crt, the Russian government services portal. For servers and development you need the Russian Trusted Root CA file in PEM format, usually named russian_trusted_root_ca_pem.crt. Don't install the intermediate: the server sends it. Besides, the national CA has two intermediates, the old Russian Trusted Sub CA and a 2024 one, and servers now send the new one, so the old intermediate file won't help.

For scripts, the root can be downloaded straight from the portal's server:

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

If that URL itself fails verification, download the file in Yandex Browser and check the fingerprint.

If a .cer file is binary, meaning it doesn't start with -----BEGIN CERTIFICATE-----, it's DER; convert it to PEM:

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

Then make sure it's the right root. It's valid until 27 February 2032, and its SHA-256 fingerprint is:

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

Which trust store your tool uses#

Installing the root in the system isn't always enough: many runtimes carry their own set of trusted roots and ignore the system's.

The system store is read by curl on Linux and Windows, Go, PowerShell, Python's urllib on Linux and Windows, and Tetiva. Python requests and httpx, Node.js and Java carry their own roots, and every Docker image has its own store inside. So install the root in the system store, then add it separately to the tools that carry their own.

Who reads the system store, and who carries their own

How to trust the root, tool by tool#

Linux

Debian and Ubuntu:

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

The file has to be PEM with a .crt extension, or update-ca-certificates skips it. The command replies 1 added.

RHEL, Fedora and relatives:

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

Alpine needs the ca-certificates package first: the base image has the bundle file but not the update-ca-certificates command. On Arch, add the root with sudo trust anchor --store russian_trusted_root_ca_pem.crt.

No admin rights (shared hosting, someone else's CI)? Build your own bundle from the system set plus the national root and pass it instead:

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

On RHEL the system set lives in /etc/pki/tls/certs/ca-bundle.crt. The file works for --cacert in curl, REQUESTS_CA_BUNDLE in requests, SSL_CERT_FILE in httpx and curl.cainfo in PHP.

macOS

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

macOS asks you to confirm the trust change in a dialog, so the command won't work over SSH or in CI. The system /usr/bin/curl sees the root afterwards, because it also checks the keychain. Recent Homebrew curl builds verify through the system too, while older ones read their own certificate file; with those, --cacert is the safe bet.

Windows

certutil -addstore -f Root russian_trusted_root_ca.cer

Run it as administrator. Without admin rights add -user, and the root is trusted for your account only. certutil takes the file from the portal as is, PEM or DER. After that, curl.exe, PowerShell, Go and Chromium-based browsers see the root.

curl for a single run

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

The CURL_CA_BUNDLE variable does the same. curl.exe on Windows verifies against the system store by default and ignores CURL_CA_BUNDLE. It does accept --cacert, but then checks against that file alone, so installing the root in the system is simpler.

Python

requests and httpx verify certificates against the certifi package and ignore the system store. Options:

  • requests: the REQUESTS_CA_BUNDLE variable or the verify= argument. It replaces the set of roots instead of adding to it: a file holding only the national root leaves requests with no other trusted roots. So add the root to the system first, then point it at the whole system bundle: REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt.
  • httpx: SSL_CERT_FILE with the same system bundle, or an ssl.create_default_context(cafile=...) context passed as verify.
  • The system store for the whole app: the truststore package, Python 3.10 or later. Two lines at app startup, and requests and httpx start verifying through macOS, Windows, or OpenSSL on Linux:
import truststore
truststore.inject_into_ssl()

The standard urllib reads the system store on Linux and Windows; on macOS a python.org or Homebrew Python ignores the keychain, so use truststore or SSL_CERT_FILE there. pip from version 24.2 on Python 3.10 and later reads the system store on its own.

Node.js

Node.js carries its own bundled roots. NODE_EXTRA_CA_CERTS adds to them and is read at process start:

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

If the root is already in the system, run node --use-system-ca app.js. The flag works on all three systems in Node.js 22.15 and later on the 22 line, in 23.9 and later, and in 24.

Java

Add the root to the shared cacerts store; its default password is changeit. The -cacerts option exists from JDK 9:

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

A separate truststore set with -Djavax.net.ssl.trustStore replaces cacerts entirely: put only the national root in it, and it holds no other trusted roots. So copy cacerts first and add the root to the copy. On Windows, Java can use the system store: -Djavax.net.ssl.trustStoreType=Windows-ROOT.

Go

Go verifies certificates through the system: on Linux it reads the bundle file and the /etc/ssl/certs directories, on macOS and Windows it asks the OS. So a root in the system is usually enough. Since Go 1.27 (for modules with go 1.27 in go.mod), SSL_CERT_FILE on macOS and Windows turns the system verifier off, and a file with a single root in it breaks verification for every other site. GODEBUG=x509sslcertoverrideplatform=0 brings the old behaviour back. In code, add the root to the system pool with x509.SystemCertPool() and AppendCertsFromPEM.

PHP

PHP uses the OpenSSL store, so a root in the system is usually enough. If you set the file explicitly in php.ini via curl.cainfo and openssl.cafile, it must hold the full set of roots, not just the national one.

git

For a single server, attach the root to that server only:

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

Git for Windows verifies against the system store by default, so certutil is enough for it.

Docker

A Docker image has its own trust store, and installing the root on your host doesn't add it to the image. In a Debian-based Dockerfile:

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 images don't include ca-certificates, so install it. Python and Node.js inside the container still won't read the system store; they need the settings from the sections above.

A trap for Mac users on OrbStack: it adds the certificates macOS trusts to your containers by itself. Everything works locally in Docker, and the same image fails in CI.

For a service that runs on Node.js, such as n8n, you don't have to rebuild the image: mount the file and set the variable in 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

If docker pull or docker login itself fails with x509: certificate signed by unknown authority, the root is needed by the Docker daemon, not the image: install it in the system and restart Docker (sudo systemctl restart docker), or put the file at /etc/docker/certs.d/<registry address>/ca.crt.

Postman, Insomnia, Bruno and Tetiva

  • Postman: Settings → Certificates tab → the "CA certificates" toggle, then pick a PEM file.
  • Insomnia: uses the system store since late 2023. You can add your own root per collection in the "Manage Certificates" dialog, under "CA Certificate"; note the file replaces the system store, so it needs the full set of roots.
  • Bruno: trusts the system store since version 2.11 too. For your own file: Preferences → General → "Use Custom CA Certificate".
  • Hoppscotch Desktop: Settings → Interceptors → Native → "CA Certificates".
  • Tetiva: verifies certificates against the system store; there's no in-app list of roots and no "don't verify" switch. A root installed in the system with the steps above is visible right away, on Linux after a restart. Until then, the request ends with this error:
A request to a national CA server in Tetiva on Linux or Windows, before the root is installed

On macOS the text differs: x509: “*.max.ru” certificate is not trusted.

Why not -k or verify=False#

-k in curl, verify=False in Python and NODE_TLS_REJECT_UNAUTHORIZED=0 in Node.js switch verification off entirely, not just for the national CA. With them the client accepts a certificate from anyone, including someone intercepting your traffic. Such a flag is easy to leave in "for now" and forget, especially in CI. With the root installed, verification can stay on and keeps protecting requests to every other site.

One more trap: a national root added to the shared system store applies to every site, not only the API you need. If that's not acceptable, keep a separate file with the root and pass it only to the requests that need it: --cacert in curl, verify= in Python, a separate truststore in Java.

Cheat sheet#

  • The sign: openssl s_client shows Russian Trusted Sub CA in the chain.
  • The root: Russian Trusted Root CA from gosuslugi.ru/crt; its SHA-256 starts with D2:6D:2D:02.
  • curl for one run: --cacert russian_trusted_root_ca_pem.crt.
  • Linux: /usr/local/share/ca-certificates/ and update-ca-certificates, or /etc/pki/ca-trust/source/anchors/ and update-ca-trust.
  • Python: REQUESTS_CA_BUNDLE or verify=. Node.js: NODE_EXTRA_CA_CERTS. Java: keytool -importcert.
  • Don't: -k, verify=False, NODE_TLS_REJECT_UNAUTHORIZED=0.