Installing Russian Trusted CA Certificates in Docker Containers
- Published on
- • 4 mins read•--- views
A payment integration to a Russian bank API suddenly started failing from inside a Docker container. Every call to https://securepay.tinkoff.ru/v2/Init died with the same line:
cURL error 60: SSL certificate problem: self signed certificate in certificate chain
The application code was fine. The trust store inside the container was not. If you run any self-hosted integration with T-Bank, Sber, or another Russian bank from a container, this will hit you too — either now, because your CA bundle is stale, or later, when the bank switches to the Russian Trusted CA (the Ministry of Digital Development root). Here is how to diagnose it properly and fix it so it survives image rebuilds.
Diagnose before you touch anything
cURL error 60 has two completely different causes, and you need to know which one you have. Reproduce the handshake from inside the exact container that makes the outbound call:
docker exec my-php-container \
openssl s_client -connect securepay.tinkoff.ru:443 -servername securepay.tinkoff.ru 2>&1 \
| grep -iE 'verify|return code'
A verify return code: 19 (self signed certificate in certificate chain) means the container does not trust the root that the server presents. Now check how old your bundle is:
docker exec my-php-container dpkg -l ca-certificates | tail -1
If you see something like 20210119, the bundle predates roots that are perfectly public today. In the case above the chain led to HARICA TLS RSA Root CA 2021 — a legitimate public CA — that a 2021 bundle simply never learned about. That is cause number one: an outdated ca-certificates package.
Cause number two is the migration Russian banks announced: when their current certificates get revoked, the API moves to the Russian Trusted CA issued by the Ministry of Digital Development. That root is in no operating system, language runtime, or HTTP library by default, and it never will be. Updating ca-certificates does nothing for it — you must install it by hand.
Install both roots
Fix the stale bundle and add the Russian roots in one pass. Download the two certificates once — the root and the intermediate:
curl -fsSL -o russian_trusted_root_ca.crt https://gu-st.ru/content/lending/russian_trusted_root_ca_pem.crt
curl -fsSL -o russian_trusted_sub_ca.crt https://gu-st.ru/content/lending/russian_trusted_sub_ca_pem.crt
On a Debian/Ubuntu-based container, drop them into the local anchor directory and refresh:
cp russian_trusted_*.crt /usr/local/share/ca-certificates/
apt-get update && apt-get install -y --only-upgrade ca-certificates
update-ca-certificates
The --only-upgrade ca-certificates step is what pulls in the newer public roots and fixes cause one. update-ca-certificates picks up the Russian roots from /usr/local/share/ca-certificates/ and fixes cause two. On Alpine it is the same anchor directory but a different package manager:
apk add --no-cache ca-certificates && update-ca-certificates
Verify it the right way
Do not check by grepping the bundle for a name — grep "Russian Trusted" ca-certificates.crt returns nothing even on a correct setup, because the bundle stores base64 DER and file labels, not subject text. That false negative wastes time. Verify by trust instead: a self-signed root only passes openssl verify against the system bundle if it is actually installed and trusted.
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt \
/usr/local/share/ca-certificates/russian_trusted_root_ca.crt
# russian_trusted_root_ca.crt: OK
Then re-run the original handshake and confirm verify return code: 0 (ok).
Make it survive a rebuild
Running those commands inside a live container fixes today and loses everything the moment you docker compose build. Bake the certificates into the image instead. Add the certs to your build context and append this to the Dockerfile:
COPY certs/russian_trusted_root_ca.crt /usr/local/share/ca-certificates/russian_trusted_root_ca.crt
COPY certs/russian_trusted_sub_ca.crt /usr/local/share/ca-certificates/russian_trusted_sub_ca.crt
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && update-ca-certificates && rm -rf /var/lib/apt/lists/*
One warning: some runtimes ignore the OS trust store. Node.js and Python read their own, so set the environment variables in the image too:
ENV NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
Do this for every image that makes outbound calls — the web container, the queue worker, and any CLI/cron container — because each has its own filesystem and its own trust store. Install the roots now, before the bank flips the switch, and the migration becomes a non-event instead of a production outage.
Open for contract collaboration
I am available for contract-based collaboration. If you have an interesting project idea, schedule a call via Calendly.
Schedule a 30-min call