Установка сертификатов Russian Trusted CA в Docker-контейнерах
- Опубликовано
- • 4 мин чтения•--- просмотров
Интеграция платежей с API российского банка внезапно начала падать изнутри Docker-контейнера. Каждый вызов https://securepay.tinkoff.ru/v2/Init умирал с одной и той же строкой:
cURL error 60: SSL certificate problem: self signed certificate in certificate chain
С кодом приложения всё было в порядке. А вот хранилище доверенных сертификатов внутри контейнера — нет. Если вы запускаете любую self-hosted интеграцию с Т-Банком, Сбером или другим российским банком из контейнера, вы столкнётесь с тем же самым — либо уже сейчас, потому что ваш набор CA устарел, либо позже, когда банк перейдёт на Russian Trusted CA (корневой сертификат Минцифры). Вот как правильно диагностировать проблему и исправить её так, чтобы она не возвращалась после пересборки образа.
Диагностика, прежде чем что-то менять
У cURL error 60 есть две совершенно разные причины, и вам нужно понимать, какая из них у вас. Воспроизведите handshake изнутри того самого контейнера, который делает исходящий вызов:
docker exec my-php-container \
openssl s_client -connect securepay.tinkoff.ru:443 -servername securepay.tinkoff.ru 2>&1 \
| grep -iE 'verify|return code'
verify return code: 19 (self signed certificate in certificate chain) означает, что контейнер не доверяет корневому сертификату, который предъявляет сервер. Теперь проверьте, насколько устарел ваш набор сертификатов:
docker exec my-php-container dpkg -l ca-certificates | tail -1
Если вы видите что-то вроде 20210119, значит набор сертификатов старше, чем вполне себе публичные корневые сертификаты сегодня. В примере выше цепочка вела к HARICA TLS RSA Root CA 2021 — легитимному публичному CA — о котором набор 2021 года просто ничего не знал. Это первая причина: устаревший пакет ca-certificates.
Вторая причина — миграция, о которой объявили российские банки: когда их текущие сертификаты будут отозваны, API перейдёт на Russian Trusted CA, выпущенный Минцифры. Этого корневого сертификата по умолчанию нет ни в одной операционной системе, ни в одном языковом рантайме или HTTP-библиотеке — и не будет никогда. Обновление ca-certificates тут не поможет: его нужно установить вручную.
Установка обоих корневых сертификатов
Исправьте устаревший набор сертификатов и добавьте российские корневые сертификаты за один проход. Скачайте оба сертификата один раз — корневой и промежуточный:
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
В контейнере на базе Debian/Ubuntu положите их в локальную директорию anchor и обновите хранилище:
cp russian_trusted_*.crt /usr/local/share/ca-certificates/
apt-get update && apt-get install -y --only-upgrade ca-certificates
update-ca-certificates
Шаг --only-upgrade ca-certificates подтягивает более новые публичные корневые сертификаты и устраняет первую причину. update-ca-certificates подхватывает российские корневые сертификаты из /usr/local/share/ca-certificates/ и устраняет вторую причину. На Alpine директория anchor та же самая, но менеджер пакетов другой:
apk add --no-cache ca-certificates && update-ca-certificates
Проверка правильным способом
Не проверяйте это через grep по имени в наборе сертификатов — grep "Russian Trusted" ca-certificates.crt не найдёт ничего даже при корректной настройке, потому что набор хранит base64 DER и метки файлов, а не текст subject. Такой ложноотрицательный результат просто тратит ваше время. Вместо этого проверяйте через доверие: самоподписанный корневой сертификат проходит openssl verify по системному набору только если он действительно установлен и доверен.
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
Затем повторите исходный handshake и убедитесь, что видите verify return code: 0 (ok).
Переживаем пересборку образа
Если выполнить эти команды внутри работающего контейнера, проблема решится сегодня, но всё потеряется в момент docker compose build. Вместо этого запеките сертификаты прямо в образ. Добавьте сертификаты в build context и допишите это в 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/*
Одно предупреждение: некоторые рантаймы игнорируют системное хранилище доверенных сертификатов. Node.js и Python читают собственное, поэтому переменные окружения тоже нужно задать в образе:
ENV NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
Сделайте это для каждого образа, который выполняет исходящие вызовы — веб-контейнера, воркера очереди и любого CLI/cron-контейнера — потому что у каждого из них своя файловая система и своё хранилище доверенных сертификатов. Установите корневые сертификаты заранее, до того как банк переключится, и миграция пройдёт незаметно, а не обернётся продакшн-инцидентом.
Открыт для работы по контракту
Я доступен для работы по контракту. Если у вас есть интересная идея проекта — запишитесь на звонок через Calendly.
Записаться на 30-минутный звонок