MAATRIX / Блог / Сертификат российского УЦ на nginx: установка по шагам

Сертификат российского УЦ на nginx: установка по шагам

MAATRIX

Сертификат от российского удостоверяющего центра по механике установки на nginx ничем принципиально не отличается от любого другого TLS-сертификата: тот же ssl_certificate, тот же приватный ключ, та же цепочка доверия. Разница в одном критичном месте — какому корневому центру доверяет клиент, который откроет сайт. Если это госсертификат, а браузер или приложение о нём не знает, соединение просто не установится, и на сервере всё будет выглядеть исправным. Разберём процесс по шагам и отдельно — этот нюанс.

Что вы получите от удостоверяющего центра

После подачи заявки и прохождения проверки домена (и, как правило, организации) УЦ выдаёт вам набор файлов. Обычно это:

  • сертификат сервера — файл вида domain.crt или domain.pem, выписанный на ваш домен;
  • приватный ключ — либо вы его генерируете сами (тогда УЦ получает только CSR — запрос на подпись, без ключа), либо УЦ выдаёт готовую пару, что менее безопасно, но встречается у некоторых центров при упрощённой процедуре;
  • промежуточные сертификаты — один или несколько файлов, связывающих ваш сертификат с корневым УЦ;
  • корневой сертификат — сам по себе на сервер обычно не идёт (об этом ниже), но пригодится для настройки доверия на стороне клиентов.

Если вы генерировали ключ сами, порядок такой: сначала создаёте приватный ключ и CSR, отправляете CSR в УЦ, получаете обратно подписанный сертификат.

# генерация приватного ключа (RSA 2048, минимально приемлемый размер на конец 2026 года)
openssl genrsa -out domain.key 2048

# запрос на подпись сертификата (CSR) — на вопросы CN и SAN отвечайте точным доменным именем
openssl req -new -key domain.key -out domain.csr -subj "/CN=example.ru"

Если сайт должен отвечать сразу на несколько имён (example.ru и www.example.ru), SAN (Subject Alternative Name) нужно указать явно — через -addext в новых версиях OpenSSL или конфиг-файл с секцией [alt_names]. Без SAN современные браузеры сертификат не примут, даже если CN указан верно.

Ключ (domain.key) никуда, кроме сервера, отправлять не нужно — это единственный секрет во всей цепочке, всё остальное публично проверяемо.

Формируем цепочку сертификатов

Это самый частый источник ошибок при установке сертификата от небраузерного или не входящего в стандартные trust store УЦ. Клиент проверяет сертификат не изолированно, а строит цепочку от вашего сертификата до корня, которому доверяет. Если промежуточное звено отсутствует на сервере, часть клиентов (в первую очередь мобильные приложения и старые ОС, которые не умеют докачивать недостающие сертификаты через AIA-расширение) откажется устанавливать соединение. Подробнее о том, почему в этой ситуации curl может сработать, а браузер — ругаться, разобрано в статье про цепочку сертификатов изнутри.

Nginx ожидает на входе один файл — сертификат сервера, за которым подряд идут промежуточные сертификаты, от ближайшего к вашему домену до ближайшего к корню. Собирается это простой конкатенацией:

cat domain.crt intermediate1.crt intermediate2.crt > domain-fullchain.crt

Порядок важен: сначала сертификат сайта, затем промежуточные — именно в том порядке, в котором каждый следующий подписывает предыдущий. Если перепутать порядок или пропустить звено, часть клиентов всё же достроит цепочку сама (через AIA fetching), а часть — откажется; полагаться на такую «доборку» не стоит, поведение отличается от клиента к клиенту.

Корневой сертификат в fullchain включать не нужно — он должен быть в доверенном хранилище клиента (или не быть, что и есть основная проблема с российскими УЦ, разобранная ниже). Включение корня не ломает работу, но раздувает handshake без пользы.

Отдельно от общей цепочки полезно сохранить сертификат для OCSP stapling — файл ssl_trusted_certificate, который должен содержать промежуточные и корневой сертификаты (но не сам сертификат сервера):

cat intermediate1.crt intermediate2.crt root.crt > domain-trusted.crt

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Настройка nginx

Минимальный конфиг серверного блока с TLS выглядит так:

server {
    listen 443 ssl;
    server_name example.ru www.example.ru;

    ssl_certificate     /etc/nginx/ssl/domain-fullchain.crt;
    ssl_certificate_key /etc/nginx/ssl/domain.key;

    ssl_trusted_certificate /etc/nginx/ssl/domain-trusted.crt;
    ssl_stapling on;
    ssl_stapling_verify on;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers off;

    root /var/www/example.ru;
    index index.html;
}

Что за что отвечает:

  • ssl_certificate — путь к файлу с сертификатом сервера и промежуточными сертификатами (тем самым fullchain, который собрали выше). Это то, что nginx реально отправляет клиенту при handshake.
  • ssl_certificate_key — путь к приватному ключу. Права на файл должны быть закрыты от посторонних (chmod 600, владелец — root или пользователь nginx-мастер-процесса).
  • ssl_trusted_certificate — используется только для OCSP stapling, не отправляется клиенту напрямую. Nginx использует его, чтобы проверить и приложить к ответу статус отозванности сертификата, полученный от OCSP-респондера УЦ.

Если ключ защищён паролем (что для боевого сервера обычно неудобно, так как требует ручного ввода при каждом рестарте), можно либо снять пароль заранее (openssl rsa -in domain.key.enc -out domain.key), либо настроить ssl_password_file.

Права на директорию с сертификатами:

mkdir -p /etc/nginx/ssl
cp domain-fullchain.crt domain.key domain-trusted.crt /etc/nginx/ssl/
chmod 600 /etc/nginx/ssl/domain.key
chmod 644 /etc/nginx/ssl/domain-fullchain.crt /etc/nginx/ssl/domain-trusted.crt

После правки конфига — проверка синтаксиса и перезагрузка без разрыва текущих соединений:

nginx -t
systemctl reload nginx

Если nginx -t ругается на несовпадение ключа и сертификата — почти всегда это означает, что в спешке подставили не ту пару файлов (например, ключ от предыдущего сертификата). Сверить, что ключ и сертификат — пара, можно по совпадению модулей:

openssl x509 -noout -modulus -in domain-fullchain.crt | openssl md5
openssl rsa -noout -modulus -in domain.key | openssl md5

Хэши должны совпасть.

Проверка корректности установки

Первым делом — проверка самим openssl, без браузера, чтобы видеть сырой ответ сервера:

openssl s_client -connect example.ru:443 -servername example.ru -showcerts

В выводе смотрите на:

  • Verify return code — в конце должно стоять Verify return code: 0 (ok). Если там unable to get local issuer certificate — значит, цепочка неполная (не хватает промежуточного сертификата) либо корень не входит в trust store машины, с которой вы проверяете (актуально при проверке с той же машины, где вы устанавливали российские корневые сертификаты, — на «чистой» машине без них ошибка будет другой природы, но выглядеть похоже).
  • Список Certificate chain — должен показывать сертификат сервера и все промежуточные звенья, в правильном порядке.

Для проверки полноты цепочки без установки российских корней в системе (то есть — что увидит клиент, у которого их нет) удобны внешние сервисы SSL-диагностики: они показывают путь построения цепочки и отсутствующие звенья. Но учитывайте: такие сервисы обычно не знают о специфических российских корневых УЦ и могут пометить цепочку как «нерабочую» даже при корректной установке — это ожидаемо, не баг конфигурации.

Локально быстрая проверка даты истечения и субъекта сертификата:

openssl x509 -in domain-fullchain.crt -noout -subject -issuer -dates

Полезно сверить subject (кому выдан) и issuer (кем выдан) — частая ошибка при копировании файлов между серверами — перепутать сертификаты разных доменов.

Если после reload сайт продолжает отдавать старый сертификат — проверьте, что действительно применился новый конфиг: nginx кэширует загруженные сертификаты только в рамках рабочих процессов, и после reload (а не полного restart) новые воркеры должны подхватить свежие файлы автоматически. Если этого не происходит — возможно, путь в ssl_certificate указывает не туда, где вы обновили файл (например, симлинк не туда смотрит), или конфиг лежит в файле, который не подключён через include. Похожий по симптомам, но самостоятельный сценарий — когда certbot продлевает сертификат, а nginx его не перечитывает; разбор такого инцидента с deploy-hook есть в статье «Сертификат обновлялся, а nginx не перечитывал его 60 дней».

Главный нюанс: доверие клиента к корневому УЦ

Это единственное отличие сертификата российского УЦ от сертификата условного международного центра, и оно не техническое, а организационное. Сертификат может быть выписан безукоризненно, цепочка может быть собрана идеально — но если корневой сертификат этого УЦ не находится в системном хранилище доверенных корней клиента, соединение всё равно будет отклонено как небезопасное.

Практически это значит:

  • Актуальные версии большинства массовых браузеров (тех, что ориентированы на международные trust store — Mozilla, Google/Chromium корневая программа) на конец августа 2026 года по умолчанию не доверяют российским государственным корневым УЦ. Пользователь увидит предупреждение о недоверенном сертификате.
  • Отечественные браузеры (в частности, те, что собираются с расчётом на российский рынок и партнёрские сборки Chromium/Firefox с добавленными корнями) и браузеры с предустановленными российскими корневыми сертификатами (через Госуслуги, КриптоПро или ручную установку) сайт откроют нормально.
  • Мобильные и десктопные приложения, которые используют собственный TLS-стек или свой pinned trust store (а не системный), сертификату не будут доверять, даже если в браузере на этом же устройстве всё работает, — если явно не добавить корень в доверенные для приложения. Похожий эффект — но по другой причине, из-за выпавшего промежуточного звена, — разобран в кейсе «Убрали промежуточный сертификат — и мобильное приложение отвалилось»: там показано, насколько чувствительны мобильные клиенты к состоянию цепочки в принципе.
  • Серверные интеграции (webhook-и, API-клиенты на других серверах, curl/wget в скриптах) будут падать с ошибкой проверки сертификата, если на машине, откуда идёт запрос, не установлен нужный корень.

Из этого следует практический вывод при выборе типа сертификата для конкретного сайта:

Аудитория сайтаСтоит ли использовать сертификат российского УЦ
Внутренний B2G-сервис, доступ только из РФ, пользователи с настроенными корнямиДа, обычно оправдано и иногда обязательно по регламенту
Публичный сайт для широкой аудитории, включая зарубежных пользователейРискованно — часть аудитории увидит предупреждение браузера
API для мобильного приложения, которое вы контролируетеМожно, но нужно явно вшить (pin) корневой сертификат в приложение
Сайт с оплатой и вводом персональных данных для розничных клиентовНежелательно без параллельного сертификата от международно доверенного УЦ — доверие пользователя к «замочку» в адресной строке напрямую влияет на конверсию

Если аудитория смешанная, практикуют двойную схему: сертификат от международно доверенного УЦ как основной (в том числе через ACME/Let's Encrypt) плюс отдельно сертификат российского УЦ для клиентов и госсистем, которым он предписан, — на поддомене или через SNI-роутинг с несколькими server блоками. Это усложняет эксплуатацию (два цикла продления вместо одного), но снимает вопрос доверия сразу для всей аудитории.

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

# Debian/Ubuntu
ls /etc/ssl/certs/ | grep -i russian

# проверка соединения с указанием, что цепочка должна замкнуться на конкретный установленный корень
openssl verify -CAfile /path/to/russian_root.pem domain-fullchain.crt

Если без -CAfile команда возвращает unable to get local issuer certificate, а с явным указанием корня — OK, это подтверждает: цепочка и сертификат корректны, а проблема ровно в том, что корень не входит в системный trust store по умолчанию.

Частые ошибки при установке

  • Указали в ssl_certificate только сам сертификат сервера, без промежуточных. Работает в браузере, который умеет докачивать недостающие звенья через AIA, и не работает в curl или мобильном приложении без такого поведения. Решение — пересобрать fullchain и убедиться, что порядок сертификатов правильный.
  • Перепутали порядок промежуточных сертификатов. Некоторые клиенты всё равно построят цепочку (перебором), другие — нет. Проверяйте порядок через openssl s_client -showcerts и сверяйте issuer одного сертификата с subject следующего.
  • Забыли обновить ssl_trusted_certificate при продлении сертификата. OCSP stapling начинает отдавать статус для старого сертификата или не работает вовсе — ssl_stapling_verify в логах nginx покажет ошибку.
  • Ключ сгенерирован с недостаточной длиной или устаревшим алгоритмом. На конец августа 2026 года RSA 2048 — минимально приемлемый вариант для большинства регламентов, но для новых установок разумно рассмотреть ECDSA (P-256), если конкретный УЦ его поддерживает, — компактнее и быстрее в handshake.
  • Файлы сертификата и ключа не синхронизированы после ручного обновления. Путь к ключу указывает на старый файл после миграции между серверами. nginx -t пройдёт (файлы физически существуют), а TLS handshake будет падать с ошибкой несовпадения ключа. Сверяйте модули, как показано выше.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Нужно ли устанавливать корневой сертификат УЦ на сам сервер с nginx?

Нет, на сервер в ssl_certificate корень не кладётся — только сертификат сайта и промежуточные звенья. Корень нужен на стороне клиента, в его доверенном хранилище, либо в ssl_trusted_certificate для целей OCSP stapling (там как раз уместны и промежуточные, и корневой).

Можно ли использовать сертификат российского УЦ вместе с Let's Encrypt на одном сервере?

Да, они не конфликтуют — это независимые пары сертификат/ключ. На одном nginx можно развести их по разным server блокам (по SNI-имени или по порту) либо чередовать по логике приложения, если нужно предъявлять разный сертификат разным категориям клиентов.

Почему curl отвечает нормально, а браузер показывает предупреждение о недоверенном сертификате?

Обычно это как раз про доверие к корню: если на машине, где запущен curl, вручную установлен нужный корневой сертификат (или указан --cacert), а в браузере — стандартный системный trust store без него, поведение будет отличаться при абсолютно одинаковой настройке сервера.

Что делать, если сайт должен одинаково хорошо открываться и у пользователей с российскими корнями, и у остальных?

Использовать сертификат от международно доверенного УЦ как основной. Если по регламенту или заказчику требуется именно российский УЦ для части трафика — разводить по SNI или поддомену, как описано выше, а не пытаться заменить один сертификат другим для всей аудитории сразу.

Как понять, что проблема именно в цепочке, а не в доверии к корню?

Прогоните openssl s_client -showcerts и openssl verify -CAfile <корень> — если с явно указанным корнем проверка проходит (OK), а без него нет, цепочка собрана правильно и вопрос ровно в доверии клиента к этому корню, а не в конфигурации сервера.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →