Важно: тумблера «Сертификаты Минцифры» в панели Bothost пока нет. Пока он не вышел, сертификаты можно подключить вручную — ниже два рабочих способа. Когда тумблер появится, достаточно будет включить его и сделать редеплой: агент сам смонтирует CA и выставит переменные окружения.
Для кого эта статья
Если бот или Mini App в мессенджере MAX ведёт себя странно с HTTPS: в браузере без VPN открывается, а в MAX без VPN нет; или Python/Node пишетCERTIFICATE_VERIFY_FAILED / unable to get local issuer certificate на запросах к российским API (ФНС, банки, госуслуги, часть сервисов вокруг MAX) — почти всегда дело в сертификатах, а не в «сломанном прокси».
В статье два разных TLS, которые путают чаще всего:
- Входящий HTTPS — что видит MAX, когда открывает ваш Mini App или вешает вебхук на
https://….bothost.tech. - Исходящий HTTPS — чем ваш контейнер на Bothost сам ходит в интернет (MAX Bot API, эквайринг, ФНС).
Зачем MAX вообще смотрит на сертификаты
MAX — не обычный Chrome. Встроенный WebView (Android) и серверы подписки на вебхук проверяют TLS строже, чем браузер на том же телефоне.- Браузер давно кэширует промежуточные CA и умеет докачать недостающее по AIA.
- MAX WebView часто не докачивает цепочку: нет промежуточного — нет доверия.
- Самоподписанный сертификат MAX не примет. Для вебхука это прямо сказано в POST /subscriptions: только HTTPS на 443, домен совпадает с CN/SAN, отдаётся полная цепочка.
- На Android системное хранилище CA обновляется медленнее Chrome. Новые промежуточные Let's Encrypt (YR1 → Root YR → ISRG Root X1, цепочка 2025–2026) браузер уже ест, а приложение — ещё нет: типичная ошибка
Trust anchor for certification path not found. С VPN путь другой, и «само проходит».
echo | openssl s_client -connect ваш-домен:443 -servername ваш-домен -showcertsBEGIN CERTIFICATE: лист сайта + промежуточный + кросс-подпись до доверенного корня (для Let's Encrypt YR1 это YR1 и Root YR, подписанный ISRG Root X1). Сам корневой ISRG Root X1 в handshake слать не нужно — он должен быть у клиента.
Подробнее про вебхук и требования к URL — в статье «Вебхуки в MAX Bot API».
Что такое сертификаты Минцифры и зачем они боту
НУЦ Минцифры (Russian Trusted CA) — российский удостоверяющий центр. Его корневой и промежуточный сертификаты нет в Mozilla CA (тот bundle, который лежит в Docker-образах Python/Node из коробки). Поэтому контейнер на Bothost по умолчанию:- спокойно ходит на Telegram, Discord, GitHub, Let's Encrypt;
- ломается на HTTPS к сайтам, которые подписаны только российским УЦ: ФНС, ряд банковских API, часть госуслуг и корпоративных шлюзов.
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED]
certificate verify failed: unable to get local issuer certificatehttps://platform-api.max.ru обычно на «обычном» коммерческом CA. Минцифры нужны, когда бот из контейнера ходит ещё и в российский контур: оплата, ФНС, внутренние API заказчика, Mini App-бэкенд на российском УЦ.
Важно не перепутать направления:
| Куда смотрит TLS | Что чинит Минцифры в контейнере | Что не чинит |
|---|---|---|
| Бот → API банков / ФНС / госов | Да: контейнер начинает доверять Russian Trusted CA | — |
| MAX → ваш Mini App / вебхук | Нет: MAX видит сертификат прокси/домена, не CA внутри контейнера | Цепочку *.bothost.tech это не меняет |
| Пользователь в Chrome → ваш сайт | Нет | Браузер уже доверяет Let's Encrypt |
Если Mini App не открывается только в MAX, а в браузере тот же URL открывается — сначала проверяйте входящую цепочку (команда openssl выше), а не кладите Минцифры в бота вслепую.
Пока нет тумблера: как включить вручную
Штатный путь Bothost (когда тумблер выйдет): агент скачает корневой и промежуточный CA с gu-st.ru, соберёт комбинированный bundle (системные CA плюс Минцифры) и выставит переменные. Комбинированный bundle обязателен: если подставить только российские CA, отвалятся Telegram, GitHub и любой обычный HTTPS. Сейчас тумблера нет — сделайте то же в коде бота или файлами в репозитории. Оба способа ниже. После любого из них нужен редploy (пересборка / «Обновить из Git»), чтобы файлы и env попали в контейнер.Способ 1. Скачать CA при старте (предпочтительный)
Официальные файлы Минцифры периодически обновляют. Скачивать их на старте проще, чем хранить протухший PEM в git. Python — положите рядом с точкой входа, вызовитеensure_mincifry_certs() до первого requests.get:
import os
import ssl
import urllib.request
from pathlib import Path
ROOT_CA = "https://gu-st.ru/content/lending/russian_trusted_root_ca_pem.crt"
SUB_CA = "https://gu-st.ru/content/lending/russian_trusted_sub_ca_pem.crt"
CERT_DIR = Path(os.getenv("CERT_DIR", "/app/certs/russian-trusted"))
def _download(url: str) -> bytes:
req = urllib.request.Request(url, headers={"User-Agent": "bothost-bot/1.0"})
with urllib.request.urlopen(req, timeout=30) as resp:
data = resp.read()
if b"BEGIN CERTIFICATE" not in data:
raise RuntimeError(f"Не сертификат: {url}")
return data if data.endswith(b"\n") else data + b"\n"
def _system_bundle() -> bytes:
for path in (
"/etc/ssl/certs/ca-certificates.crt",
"/etc/ssl/cert.pem",
"/etc/pki/tls/certs/ca-bundle.crt",
):
p = Path(path)
if p.is_file():
data = p.read_bytes()
return data if data.endswith(b"\n") else data + b"\n"
try:
import certifi
data = Path(certifi.where()).read_bytes()
return data if data.endswith(b"\n") else data + b"\n"
except Exception:
return b""
def ensure_mincifry_certs() -> Path:
CERT_DIR.mkdir(parents=True, exist_ok=True)
russian = _download(ROOT_CA) + _download(SUB_CA)
(CERT_DIR / "russian_trusted_ca_bundle.crt").write_bytes(russian)
combined = _system_bundle() + russian
combined_path = CERT_DIR / "ca-bundle.crt"
combined_path.write_bytes(combined)
os.environ["SSL_CERT_FILE"] = str(combined_path)
os.environ["REQUESTS_CA_BUNDLE"] = str(combined_path)
os.environ["CURL_CA_BUNDLE"] = str(combined_path)
os.environ["NODE_EXTRA_CA_CERTS"] = str(CERT_DIR / "russian_trusted_ca_bundle.crt")
ssl.create_default_context() # прогрев, чтобы процесс подхватил SSL_CERT_FILE
return combined_path
if __name__ == "__main__":
ensure_mincifry_certs()
# дальше обычный запуск ботаrequests / httpx. Если библиотека уже создала пул SSL — перезапустите процесс (на Bothost это редеплой).
Node.js — Минцифры достаточно как дополнительных CA (NODE_EXTRA_CA_CERTS не затирает встроенные):
const fs = require("fs");
const path = require("path");
const https = require("https");
const ROOT = "https://gu-st.ru/content/lending/russian_trusted_root_ca_pem.crt";
const SUB = "https://gu-st.ru/content/lending/russian_trusted_sub_ca_pem.crt";
const dir = process.env.CERT_DIR || "/app/certs/russian-trusted";
async function download(url) {
const res = await fetch(url, { headers: { "User-Agent": "bothost-bot/1.0" } });
const text = await res.text();
if (!text.includes("BEGIN CERTIFICATE")) throw new Error("Не сертификат: " + url);
return text.endsWith("\n") ? text : text + "\n";
}
async function ensureMincifryCerts() {
fs.mkdirSync(dir, { recursive: true });
const russian = (await download(ROOT)) + (await download(SUB));
const russianPath = path.join(dir, "russian_trusted_ca_bundle.crt");
fs.writeFileSync(russianPath, russian);
process.env.NODE_EXTRA_CA_CERTS = russianPath;
}
ensureMincifryCerts().then(() => {
require("./bot.js");
});NODE_EXTRA_CA_CERTS Node читает файл при старте процесса. Поэтому скачивание должно быть в отдельном маленьком start.js, который потом require основной файл — не наоборот.
Способ 2. Файлы в репозитории + переменные в панели
Если бот в сети, откудаgu-st.ru недоступен в момент старта, положите PEM в git (каталог certs/) и соберите combined bundle один раз на своей машине:
curl -fsSL https://gu-st.ru/content/lending/russian_trusted_root_ca_pem.crt -o certs/root.crt
curl -fsSL https://gu-st.ru/content/lending/russian_trusted_sub_ca_pem.crt -o certs/sub.crt
cat certs/root.crt certs/sub.crt > certs/russian_trusted_ca_bundle.crt
# combined: системный bundle + российские
cat /etc/ssl/certs/ca-certificates.crt certs/russian_trusted_ca_bundle.crt > certs/ca-bundle.crtSSL_CERT_FILE | /app/certs/ca-bundle.crt | OpenSSL / Python ssl |
| REQUESTS_CA_BUNDLE | /app/certs/ca-bundle.crt | requests, часть httpx |
| CURL_CA_BUNDLE | /app/certs/ca-bundle.crt | curl внутри контейнера |
| NODE_EXTRA_CA_CERTS | /app/certs/russian_trusted_ca_bundle.crt | Node, только российские |
Путь /app/... — рабочая директория контейнера Bothost. Если код лежит не в корне образа, поправьте путь.
Нельзя ставить SSL_CERT_FILE / REQUESTS_CA_BUNDLE на файл, где лежат только сертификаты Минцифры. Тогда отвалится исходящий HTTPS к Telegram и к самому MAX Bot API.
После сохранения переменных — редploy. Env без пересборки контейнера у уже запущенного процесса не подхватится.
Как проверить, что заработало
В том же контейнере (логи бота или одноразовый скрипт):import os
import requests
print("SSL_CERT_FILE", os.getenv("SSL_CERT_FILE"))
print("REQUESTS_CA_BUNDLE", os.getenv("REQUESTS_CA_BUNDLE"))
# Замените URL на тот, который у вас падал
r = requests.get("https://ваш-российский-api.example", timeout=20)
print(r.status_code)require("https").get("https://ваш-российский-api.example", (res) => {
console.log(res.statusCode);
}).on("error", console.error);unable to get local issuer certificate и после редеплоя ушёл 200 — Минцифры на месте.
Проверка входящей цепочки для Mini App / вебхука (это не Минцифры в контейнере):
echo | openssl s_client -connect bot-xxxx.bothost.tech:443 -servername bot-xxxx.bothost.tech -showcertsVerify return code: 0 (ok).
Когда Минцифры не нужны
- Обычный Telegram / Discord бот без российских HTTPS API.
- Long polling к
platform-api.max.ru, если этот хост у вас и так открывается без SSL-ошибки. - «MAX не открывает Mini App, а Chrome открывает» — сначала цепочка входящего сертификата домена, не CA внутри бота.
Что будет, когда появится тумблер
В панели у бота появится флажок «Сертификаты Минцифры». Он запишет флаг в биллинг; со следующего редеплоя агент:- скачает CA с gu-st.ru на ноду;
- примонтирует их в контейнер в
/usr/local/share/ca-certificates/russian-trusted(только чтение); - выставит
SSL_CERT_FILE,REQUESTS_CA_BUNDLE,CURL_CA_BUNDLEна комбинированный bundle иNODE_EXTRA_CA_CERTSна российский.
Коротко
- MAX проверяет HTTPS строже браузера: нужна полная цепочка на домене Mini App / вебхука.
- Минцифры — это доверие из контейнера к российским УЦ. Они не подменяют сертификат
*.bothost.tech. - Тумблера в панели пока нет: скачайте CA с gu-st.ru при старте или положите PEM в репозиторий и пропишите env, затем сделайте редеплой.
SSL_CERT_FILEвсегда на системные CA + Минцифры, иначе сломаете Telegram и обычный интернет бота.
9 просмотров
0 лайков
0 комментариев
Комментарии (0)
Пока нет комментариев. Будьте первым!