Skip to content
EN
← Все статьи

Цепочка сертификатов SSL: как проверить и починить неполную

Коротко. Цепочка сертификатов — это путь от сертификата вашего сайта до корневого центра сертификации, которому доверяет клиент. Сервер обязан отдать листовой сертификат и все промежуточные; корневой клиент берёт из собственного хранилища доверия. Если промежуточного нет, десктопный браузер часто выкрутится сам, а curl, Java, Python и мобильные приложения — нет.

Что такое цепочка сертификатов

TLS-сертификат сам по себе ничего не доказывает: это просто файл, где написано «владелец домена example.com, вот его открытый ключ». Доверие возникает только тогда, когда клиент может проследить, кто этот файл подписал, кто подписал подписавшего — и так до сертификата, который уже лежит у клиента в списке доверенных. Эта последовательность и называется цепочкой сертификатов (certificate chain, chain of trust).

Три уровня: листовой, промежуточный, корневой

Root CA — корневой, самоподписанный, лежит в trust store клиента
  └── Intermediate CA — промежуточный, подписан корневым
        └── Leaf (end-entity) — ваш сертификат на example.com
  • Листовой (leaf, end-entity) — сертификат конкретного домена. В нём есть SAN со списком имён, срок действия, открытый ключ сервера и подпись промежуточного УЦ. Только он привязан к вашему приватному ключу.
  • Промежуточный (intermediate) — рабочая лошадка УЦ. Именно им подписываются миллионы листовых сертификатов. Промежуточных в цепочке может быть ноль, один или несколько.
  • Корневой (root)самоподписанный сертификат: поле Issuer совпадает с полем Subject. Его приватный ключ хранится офлайн и достаётся крайне редко, поэтому им не подписывают клиентские сертификаты напрямую.

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

Почему корневой сертификат не передаётся по сети

Смысл корневого сертификата в том, что клиент уже имеет его локально и заранее ему доверяет. Если сервер пришлёт свою копию корня, клиент ей всё равно не поверит «за то, что она пришла» — он сверит её со своим хранилищем. Отправка корня не даёт ничего, кроме лишних байт в хендшейке (типичный корневой сертификат — 1–2 КБ) и лишнего круга при медленном соединении.

Правило: сервер отдаёт листовой сертификат и все промежуточные до корня, но не сам корень. SSL Labs помечает лишний корень как «Chain issues: Contains anchor» — это не ошибка валидации, но и не норма.

Что именно отдаёт сервер в TLS-хендшейке

В сообщении Certificate сервер передаёт список сертификатов. RFC 8446, §4.4.2 (TLS 1.3) требует, чтобы листовой сертификат был первым; порядок остальных формально стал рекомендацией, и реализациям предписано быть готовыми к произвольному порядку и лишним сертификатам. В RFC 5246, §7.4.2 (TLS 1.2) порядок был жёстким требованием. На практике это значит: современные клиенты чаще всего разберут перепутанный порядок, старые — нет. Собирайте файл правильно.

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

Trust store: где клиент хранит корневые сертификаты

Trust store (хранилище доверия, корневое хранилище) — это набор корневых сертификатов, которым система или приложение доверяет безусловно. Ключевой момент, который ломает половину диагностик: trust store у каждой платформы свой, и они не совпадают. Сертификат, валидный в Chrome на Windows, может быть невалиден для Java-приложения на том же самом компьютере.

Системные хранилища

ПлатформаГде хранятся корниЧем посмотреть
WindowsСистемное хранилище сертификатов, раздел «Доверенные корневые центры сертификации»; корни доезжают через механизм автообновленияcertlm.msc, PowerShell: Get-ChildItem Cert:\LocalMachine\Root
macOS / iOSСвязка ключей System RootsKeychain Access; security find-certificate -a -p /System/Library/Keychains/SystemRootCertificates.keychain
Debian / UbuntuПакет ca-certificates, собранный бандл /etc/ssl/certs/ca-certificates.crtls /usr/share/ca-certificates/, добавление — в /usr/local/share/ca-certificates/ + update-ca-certificates
RHEL / AlmaLinux / RockyБандл /etc/pki/tls/certs/ca-bundle.crtдобавление — в /etc/pki/ca-trust/source/anchors/ + update-ca-trust
AlpineПакет ca-certificates; в базовом образе его может не быть вовсеapk add ca-certificates

Свои хранилища: Firefox, Chrome, Java, Python, Node.js

  • Firefox использует собственное хранилище NSS со списком корней Mozilla и по умолчанию игнорирует системное. Исключение — корпоративные корни: на Windows и macOS Firefox умеет подхватывать их из системы (настройка security.enterprise_roots.enabled в about:config). Именно поэтому «в Chrome сайт открывается, в Firefox — нет» — почти всегда история про хранилище, а не про сервер.
  • Chrome в современных версиях использует собственный Chrome Root Store и встроенный верификатор вместо платформенного, но продолжает уважать корни, добавленные администратором в системное хранилище. Поэтому корпоративный MITM-прокси в Chrome работает, а произвольный «свой» корень в системе — уже не гарантия.
  • Java хранит корни в keystore cacerts внутри дистрибутива JDK/JRE ($JAVA_HOME/lib/security/cacerts, в Java 8 — $JAVA_HOME/jre/lib/security/cacerts). Управляется keytool. Обновляется вместе с обновлением JDK — на серверах, где JDK не трогали годами, набор корней соответствующий.
  • Python с библиотекой requests по умолчанию использует пакет certifi — снимок корневого списка Mozilla, вшитый в виртуальное окружение. Он обновляется вместе с пакетом, а не с ОС.
  • Node.js носит встроенную копию корневого списка Mozilla. Добавить свои корни можно переменной NODE_EXTRA_CA_CERTS; в свежих ветках появился флаг для использования системного хранилища.
  • Go на Linux читает системные бандлы, на macOS и Windows обращается к платформенным API.

Android и мобильные приложения

На Android системные корни поставляются вместе с прошивкой (в новых версиях часть обновляется отдельным модулем). Отсюда классическая беда старых устройств: свежий корневой сертификат в них просто не появится, пока не обновится ОС, а на устройствах без обновлений это «никогда». Второй нюанс: начиная с Android 7 приложения по умолчанию не доверяют пользовательским корням — добавить сертификат вручную и ожидать, что заработает приложение, а не только браузер, не выйдет без правки Network Security Config.

Проверяя «а работает ли сайт», всегда спрашивайте себя: у какого именно клиента. Ответ «работает» без указания клиента бессмысленен — trust store у браузера, у curl и у мобильного приложения разные.

Отдельный случай — корни, которых нет ни в одном публичном хранилище, но которые распространяются в отдельной стране или отдельной компании. Как это устроено у российских удостоверяющих центров, разобрано в отдельном материале: SSL-сертификаты российских УЦ.

Path building: почему цепочка — это не список, а дерево путей

Термин «цепочка» вводит в заблуждение. Клиент не «читает список сертификатов сверху вниз» — он выполняет построение пути сертификации (certification path building) по правилам RFC 5280, §6. Алгоритм такой: взять листовой сертификат, найти кандидатов на роль издателя (по полю Issuer и по Authority Key Identifier), для каждого кандидата попробовать продолжить путь, пока не упрёмся в доверенный якорь. Кандидатов может быть несколько — значит, и путей несколько.

Кросс-подписи: один ключ, два разных сертификата

Кросс-подпись (cross-signing) — это когда один и тот же открытый ключ УЦ упакован в два разных сертификата, подписанных разными издателями. Subject и открытый ключ одинаковые, Issuer — разный. Зачем: новый УЦ выходит на рынок, его корня ещё нет в устройствах, которые не обновлялись. Он просит старый, широко распространённый УЦ подписать свой корень — и получает второй путь, работающий на старых клиентах.

Хрестоматийный пример — Let's Encrypt: их корень ISRG Root X1 много лет был дополнительно подписан корнем DST Root CA X3 от IdenTrust, что давало совместимость со старыми устройствами. Их же ECDSA-корень ISRG Root X2 кросс-подписан корнем ISRG Root X1 — чтобы клиенты, знающие только X1, тоже могли построить путь.

# Путь 1 (короткий): leaf → R11 → ISRG Root X1 (самоподписанный, в trust store)
# Путь 2 (длинный):  leaf → R11 → ISRG Root X1 (кросс-подписан) → DST Root CA X3

# Один и тот же сервер, одна и та же цепочка на диске —
# но разные клиенты приходят к разным якорям доверия.

Истечение корня: почему одни клиенты ломаются, а другие нет

Самый показательный инцидент отрасли — истечение корня DST Root CA X3 в 2021 году. Сервера не менялись, сертификаты были валидны, но часть клиентов начала отваливаться, а часть продолжила работать. Причины ровно три, и все три — про построение пути:

  1. Клиенты с ISRG Root X1 в хранилище находили короткий путь и завершали проверку на нём. Истёкший DST Root CA X3 их не касался.
  2. Клиенты со старым OpenSSL (ветка 1.0.2) находили длинный путь через истёкший корень, получали ошибку и не пробовали альтернативу — механизм перебора альтернативных путей появился в более поздних ветках. Формально валидный путь существовал, но библиотека до него не доходила.
  3. Старый Android продолжил работать именно потому, что его верификатор не проверяет срок действия самого якоря доверия. Истёкший корень для него оставался доверенным.

Вывод, который переживёт этот конкретный инцидент: «сертификат валиден» — свойство пары «сертификат + клиент», а не свойство сервера. Один и тот же ответ сервера одновременно валиден для одного клиента и невалиден для другого, и это штатное поведение PKI, а не баг.

Позже Let's Encrypt прекратил выдавать кросс-подписанную цепочку через DST Root CA X3, и сама кросс-подпись истекла. Опция certbot --preferred-chain "DST Root CA X3", которую до сих пор советуют в старых инструкциях, больше не даёт эффекта. Актуальный список цепочек всегда есть на странице сертификатов Let's Encrypt.

AIA и caIssuers: кто умеет догружать промежуточный

В сертификате есть расширение Authority Information Access (AIA). В нём два поля: OCSP — адрес сервиса проверки отзыва, и caIssuers — прямой URL, по которому лежит сертификат издателя. Некоторые клиенты, не найдя промежуточный в присланной цепочке, идут по этому URL и догружают его сами. Такое поведение называют AIA fetching или AIA chasing.

# Посмотреть AIA у сертификата
openssl x509 -in cert.pem -noout -ext authorityInfoAccess

# Старый вариант, работает везде
openssl x509 -in cert.pem -noout -text | grep -A2 "Authority Information Access"

# Скачать промежуточный по caIssuers (обычно DER) и превратить в PEM
curl -sS http://r11.i.lencr.org/ | openssl x509 -inform DER -out intermediate.pem

Именно AIA — причина главного эффекта, из-за которого неполная цепочка живёт годами незамеченной: клиенты, умеющие AIA fetching, чинят вашу конфигурацию за вас, а те, кто не умеет, падают. Второй маскирующий фактор — кеш: браузер, однажды увидевший промежуточный на другом сайте, может использовать его повторно, поэтому «у меня открывается» вообще ничего не доказывает.

Никогда не полагайтесь на AIA как на замену правильной цепочке. Догрузка по AIA — это лишний HTTP-запрос в критическом пути хендшейка, к чужому хосту, часто по plain HTTP, и без гарантий доступности. Промежуточные должны быть в файле на вашем сервере.

Почему в браузере работает, а curl, Java и Python падают

Это самый частый практический сценарий: сайт открывается в браузере, оценка мониторинга зелёная, а интеграция по API отваливается. Причина — разное сочетание двух свойств клиента: откуда он берёт корни и умеет ли догружать промежуточные.

КлиентОткуда берёт корниДогружает по AIAТипичное сообщение при неполной цепочке
Chrome (Android)Хранилище AndroidНетERR_CERT_AUTHORITY_INVALID — на том же сайте, который открывается на десктопе
Chrome (десктоп)Chrome Root Store + корпоративные корни системыДаERR_CERT_AUTHORITY_INVALID (если догрузить не удалось)
FirefoxСобственный NSS-store MozillaНет; использует кеш ранее виденных и предзагруженный список промежуточныхSEC_ERROR_UNKNOWN_ISSUER
Safari, iOSСистемная связка ключейДа«Не удаётся установить защищённое соединение»
Windows: schannel, .NET, PowerShellСистемное хранилищеДаThe remote certificate is invalid
curl + OpenSSL / GnuTLSCA-бандл ОСНетunable to get local issuer certificate, код выхода 60
Java (PKIX)cacerts внутри JDKНет по умолчанию; включается системным свойством com.sun.security.enableAIAcaIssuersPKIX path building failed: unable to find valid certification path to requested target
Python requestsБандл certifiНет[SSL: CERTIFICATE_VERIFY_FAILED] unable to get local issuer certificate
GoСистемное хранилище платформыНетx509: certificate signed by unknown authority
Node.jsВстроенный список MozillaНетUNABLE_TO_VERIFY_LEAF_SIGNATURE или UNABLE_TO_GET_ISSUER_CERT_LOCALLY
Мобильное приложение (Android/iOS SDK)Системное хранилище устройстваОбычно да, но зависит от сетевого слояОбрыв на этапе установки соединения, часто без внятного текста

Отдельно стоит запомнить Chrome на Android: в отличие от десктопного, он по AIA промежуточные не догружает. Поэтому «на компьютере открывается, на телефоне — нет» при одном и том же браузере — это не глюк телефона, а ровно неполная цепочка.

Практический вывод: тестировать нужно тем клиентом, который реально ходит на ваш сервер. Если сайт потребляет мобильное приложение и платёжный шлюз на Java, зелёный замок в Chrome не является доказательством работоспособности.

Проверка цепочки сертификатов: команды

Дальше — рабочий набор. Все команды безопасны, ничего не меняют и выполняются с любой машины, у которой есть сетевой доступ к серверу.

openssl s_client: что реально отдаёт сервер

openssl s_client -showcerts -connect example.com:443 -servername example.com </dev/null

Разбор аргументов:

  • -showcerts — вывести PEM всех сертификатов, которые прислал сервер, а не только листового.
  • -servername example.com — передать SNI. Без него сервер с несколькими сайтами отдаст сертификат по умолчанию, и вы будете диагностировать чужую цепочку. Это ошибка номер один в диагностике.
  • </dev/null — закрыть stdin, иначе команда зависнет в интерактивном режиме.

Быстрый вариант — просто посчитать, сколько сертификатов пришло:

openssl s_client -showcerts -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | grep -c "BEGIN CERTIFICATE"

Для типичного публичного сайта нормальный ответ — 2 (лист + один промежуточный), иногда 3. Ответ 1 — почти наверняка неполная цепочка.

Как читать блок Certificate chain

В начале вывода s_client идёт блок с нумерованными строками s: (subject) и i: (issuer):

Certificate chain
 0 s:CN=example.com
   i:C=US, O=Let's Encrypt, CN=R11
 1 s:C=US, O=Let's Encrypt, CN=R11
   i:C=US, O=Internet Security Research Group, CN=ISRG Root X1

Проверять надо одно правило: issuer сертификата N должен совпадать с subject сертификата N+1. В примере выше i: нулевого совпадает с s: первого — цепочка сцеплена. Последний i: (здесь ISRG Root X1) — это корень, который сервер не отдаёт и который должен быть у клиента.

Что должно насторожить:

  • Есть только запись с номером 0 — сервер отдаёт один сертификат, промежуточных нет.
  • i: одного сертификата не совпадает ни с одним s: ниже — файл собран из кусков от разных выпусков.
  • В цепочке есть сертификат, у которого s: равен i: — это самоподписанный корень, его отдавать не нужно.
  • Сертификатов больше трёх и часть из них явно лишние — «Extra download» в терминах SSL Labs.

Verify return code: расшифровка

В самом конце вывода s_client печатает результат проверки со стороны OpenSSL. Это самая полезная строка во всём выводе.

Что показал инструментЧто это значит
Verify return code: 0 (ok)Путь построен и проверен до доверенного корня. Цепочка в порядке — для этой машины и этого CA-бандла.
21 (unable to verify the first certificate)Сервер прислал только листовой сертификат, издателя найти негде. Классическая неполная цепочка.
20 (unable to get local issuer certificate)Цепочка выстроена, но её корня нет в хранилище клиента. Либо УЦ не публичный (корпоративный, ведомственный), либо бандл устарел.
2 (unable to get issuer certificate)Не найден издатель для промежуточного — в файле пропущено звено в середине цепочки.
10 (certificate has expired)Истёк какой-то элемент пути. Не обязательно листовой — промежуточные тоже имеют срок.
18 (self signed certificate)Листовой сертификат самоподписанный — это отдельная история, см. самоподписанные сертификаты.
19 (self signed certificate in certificate chain)В цепочку положили корень, которому эта машина не доверяет. Убрать корень из файла и/или добавить его в хранилище клиента.
62 (hostname mismatch)Цепочка валидна, но имя в сертификате не совпадает с запрошенным. К цепочке отношения не имеет.

Помните: Verify return code: 0 означает «ок на этой машине». Если вы проверяете с ноутбука со свежей macOS, а падает старый Android — результат не переносится. Проверять надо с окружения, максимально похожего на проблемное.

Терминал с выводом openssl s_client: блок Certificate chain и строка Verify return code
Блок Certificate chain показывает, что реально прислал сервер, строка Verify return code — вердикт проверки.

openssl verify: проверить цепочку локально

Если сертификаты уже лежат файлами, цепочку можно проверить без сети:

# Проверить лист по известному промежуточному и системным корням
openssl verify -untrusted chain.pem cert.pem

# Проверить против конкретного корня, игнорируя системное хранилище
openssl verify -CAfile root.pem -untrusted chain.pem cert.pem

# Показать построенный путь целиком
openssl verify -show_chain -untrusted chain.pem cert.pem

Разница между флагами принципиальная: -CAfile задаёт якоря доверия (корни), -untrustedпромежуточные, которые можно использовать при построении пути, но которым не доверяют сами по себе. Частая ошибка — засунуть промежуточный в -CAfile: проверка пройдёт, но результат ничего не докажет, потому что вы объявили промежуточный доверенным якорем.

Разобрать PEM-файл на сертификаты

PEM-бандл — это просто несколько блоков BEGIN CERTIFICATE подряд. Чтобы увидеть, что именно в файле и в каком порядке:

# Классический способ, работает на любой версии OpenSSL
openssl crl2pkcs7 -nocrl -certfile fullchain.pem \
  | openssl pkcs7 -print_certs -noout

# Современная альтернатива
openssl storeutl -noout -text -certs fullchain.pem | grep -E "Subject:|Issuer:"

# Только даты и имена по одному файлу
openssl x509 -in cert.pem -noout -subject -issuer -dates

Вывод первой команды выглядит так:

subject=CN=example.com
issuer=C=US, O=Let's Encrypt, CN=R11

subject=C=US, O=Let's Encrypt, CN=R11
issuer=C=US, O=Internet Security Research Group, CN=ISRG Root X1

Проверяется то же правило сцепки: issuer первого равен subject второго. Если у вас в файле «issuer второго» не равен «subject третьего» — звено потеряно.

Сопоставить лист и промежуточный можно и точнее — по идентификаторам ключей. Authority Key Identifier листового должен совпадать с Subject Key Identifier промежуточного:

openssl x509 -in cert.pem   -noout -ext authorityKeyIdentifier
openssl x509 -in chain.pem  -noout -ext subjectKeyIdentifier

curl, keytool и «чистый клиент» в Docker

# curl показывает построенную цепочку и вердикт
curl -vI https://example.com 2>&1 | grep -Ei "subject:|issuer:|SSL certificate|verify"

# Явно указать бандл — проверить, дело в цепочке или в хранилище
curl --cacert /etc/ssl/certs/ca-certificates.crt -sS -o /dev/null https://example.com

# Что видит Java: печатает цепочку, которую отдал сервер
keytool -printcert -sslserver example.com:443

# «Чистый» клиент без десктопных кешей и корпоративных корней
docker run --rm alpine sh -c "apk add --no-cache ca-certificates curl >/dev/null && curl -sSI https://example.com | head -1"

Тест из свежего контейнера — самый честный: там нет ни браузерного кеша промежуточных, ни корней, которые кто-то когда-то добавил вручную. Если в контейнере работает, а на проде нет — проблема на стороне клиента, а не сервера.

Порядок сертификатов в файле и чем отличаются cert.pem, chain.pem и fullchain.pem

Certbot кладёт в каталог живого сертификата четыре файла, и половина инцидентов с цепочкой начинается с того, что в конфиг вписали не тот:

ФайлЧто внутриКуда обычно идёт
privkey.pemПриватный ключssl_certificate_key в nginx, SSLCertificateKeyFile в Apache
cert.pemТолько листовой сертификатApache до 2.4.8 (SSLCertificateFile) в паре с SSLCertificateChainFile. В nginx — почти всегда ошибка
chain.pemТолько промежуточные, без листа и без корняSSLCertificateChainFile; в nginx — ssl_trusted_certificate для OCSP stapling
fullchain.pemЛист + промежуточные, в правильном порядкеssl_certificate в nginx, SSLCertificateFile в Apache 2.4.8 и новее

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

# fullchain.pem
-----BEGIN CERTIFICATE-----   ← 1. лист (example.com)
-----BEGIN CERTIFICATE-----   ← 2. промежуточный, подписавший лист
-----BEGIN CERTIFICATE-----   ← 3. промежуточный выше (если есть)
#                               корень НЕ добавляем

Собрать вручную:

cat cert.crt intermediate.crt > fullchain.crt

# Проверить, что получилось, ДО перезагрузки сервиса
openssl crl2pkcs7 -nocrl -certfile fullchain.crt | openssl pkcs7 -print_certs -noout
openssl verify -untrusted fullchain.crt cert.crt

Частая ловушка: файлы от коммерческого УЦ приходят в архиве с именами вида example_com.crt, SectigoRSADomainValidationSecureServerCA.crt, USERTrustRSAAAACA.crt, AAACertificateServices.crt. Порядок в архиве и порядок в цепочке не совпадают. Не собирайте файл по алфавиту — собирайте по полям issuer/subject.

Что бывает при обратном порядке: часть клиентов (в основном современные, по духу RFC 8446) разберётся, часть — нет. GnuTLS, старые сборки OpenSSL и некоторые Java-стеки отдают ошибку. Хуже всего то, что проблема плавающая: она видна не у всех и не всегда, поэтому её долго не признают проблемой.

Неполная цепочка: симптомы и починка

Incomplete chain — частный, но самый массовый случай поломки цепочки: сервер отдаёт только листовой сертификат без промежуточных.

Симптомы

  • Chrome на десктопе работает, Safari на iPhone — нет.
  • curl падает с unable to get local issuer certificate.
  • Java-приложение: PKIX path building failed.
  • Python requests: [SSL: CERTIFICATE_VERIFY_FAILED].
  • Телеграм-бот не принимает webhook, хотя сертификат валиден.
  • Платёжный шлюз или контрагент по API сообщает о недоверенном сертификате, а вы «ничего не меняли».
  • SSL Labs понижает оценку до B с пометкой «Chain issues: Incomplete».
  • Проблема появилась ровно после обновления сертификата — типично для самописных скриптов деплоя.

Сборка правильного fullchain

Для Let's Encrypt и любого ACME-клиента ничего собирать не надо — fullchain.pem уже готов:

# Правильно
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;

# Неправильно: только лист, промежуточного нет
ssl_certificate /etc/letsencrypt/live/example.com/cert.pem;

Если сертификат от коммерческого УЦ — соберите файл вручную по правилу порядка из предыдущего раздела и проверьте результат openssl verify до релоада.

Настройка веб-серверов и прокси

# nginx
ssl_certificate           /etc/nginx/certs/fullchain.pem;
ssl_certificate_key       /etc/nginx/certs/privkey.pem;
ssl_trusted_certificate   /etc/nginx/certs/chain.pem;   # для OCSP stapling
nginx -t && systemctl reload nginx
# Apache 2.4.8 и новее — всё в одном файле
SSLCertificateFile      /etc/apache2/certs/fullchain.pem
SSLCertificateKeyFile   /etc/apache2/certs/privkey.pem

# Apache до 2.4.8 — цепочка отдельно
SSLCertificateFile      /etc/apache2/certs/cert.pem
SSLCertificateChainFile /etc/apache2/certs/chain.pem

apachectl configtest && systemctl reload apache2

Другие популярные точки терминации TLS:

  • HAProxy — один PEM-файл, в котором подряд лежат приватный ключ, лист и промежуточные.
  • Traefik — в файловом провайдере поле сертификата должно содержать полную цепочку, а не только лист.
  • IIS — промежуточные импортируются в хранилище «Промежуточные центры сертификации» на самой машине, а не прикладываются к сайту.
  • Tomcat и другие Java-серверы — цепочка должна быть внутри keystore целиком; импорт одного листа в существующий keypair оставляет цепочку из одного элемента.
  • Postfix, Dovecot, почтовые серверы — почтовые клиенты и MTA почти никогда не умеют AIA, поэтому неполная цепочка ломает их сразу и жёстко. Проверять почтовые порты отдельно.
  • Балансировщики облаков и CDN — обычно есть отдельное поле «certificate chain»; оставленное пустым, оно даёт ровно ту же проблему.

Если TLS терминируется на балансировщике или CDN, конфигурация сертификата на бэкенде не влияет на то, что видит пользователь. Проверять надо публичный адрес, а не origin.

Где взять потерянный промежуточный

  1. AIA внутри вашего же сертификата — самый надёжный путь, потому что даёт ровно тот промежуточный, который подписал именно ваш лист:
    openssl x509 -in cert.pem -noout -ext authorityInfoAccess
    curl -sS http://<caIssuers-url> | openssl x509 -inform DER -out intermediate.pem
  2. Сайт удостоверяющего центра — у Let's Encrypt это letsencrypt.org/certificates, у коммерческих УЦ есть аналогичные разделы с репозиторием промежуточных.
  3. Логи Certificate Transparency — найдите свой сертификат на crt.sh и перейдите по ссылке на издателя.
  4. Переиздать сертификат — если УЦ сменил промежуточный, старый может уже не подходить. Перевыпуск через ACME-клиент решает вопрос за минуту, см. выпуск сертификата Let's Encrypt.
Сравнение полной и неполной цепочки: сервер отдаёт лист и промежуточный против только листа
Слева — сервер отдаёт лист и промежуточный, путь строится. Справа — только лист, часть клиентов путь не построит.

Разбор типовых ошибок: симптом → причина → проверка → фикс

СимптомПричинаЧем проверитьЧто сделать
unable to get local issuer certificate в curl, код 60 Сервер не отдал промежуточный, а в бандле клиента только корни openssl s_client -showcerts … | grep -c "BEGIN CERT" → 1 Собрать fullchain и перезагрузить сервис
PKIX path building failed в Java То же самое; Java не догружает по AIA keytool -printcert -sslserver host:443 Починить цепочку на сервере. Импорт корня в cacerts — костыль, не решение
SEC_ERROR_UNKNOWN_ISSUER только в Firefox Firefox не ходит по AIA и не видел ваш промежуточный раньше Открыть тот же URL в приватном окне другого браузера Добавить промежуточный в цепочку сервера
x509: certificate signed by unknown authority в Go-сервисе Неполная цепочка либо непубличный корень openssl verify -CAfile с корнем УЦ Если корень непубличный — раздать его клиентам явно, а не отключать проверку
Ошибка появилась после продления сертификата Скрипт деплоя копирует cert.pem вместо fullchain.pem, либо промежуточный УЦ сменился Сравнить вывод s_client до и после, посмотреть CN промежуточного Исправить путь в скрипте деплоя, добавить постпроверку в хук renewal
Ошибка только на старых устройствах Корня УЦ нет в прошивке, а кросс-подписанный путь недоступен Проверить, какие корни знает устройство; посмотреть альтернативные цепочки УЦ Выбрать УЦ или цепочку с более широкой совместимостью; для критичной аудитории — отдельный сертификат
SSL Labs: «Chain issues: Incorrect order» Сертификаты в файле идут не по порядку openssl crl2pkcs7 … | openssl pkcs7 -print_certs -noout Пересобрать файл: лист первым, дальше вверх по дереву
SSL Labs: «Chain issues: Contains anchor» В цепочку включён корневой сертификат Тот же вывод: последний сертификат самоподписанный Убрать корень из файла (не критично, но лишний трафик)
Verify return code: 10, при этом лист свежий Истёк промежуточный сертификат openssl x509 -in chain.pem -noout -dates Скачать актуальный промежуточный у УЦ или перевыпустить сертификат
В браузере ошибка про недоверенный УЦ Отдельная тема: корня нет в хранилище или подмена трафика Сравнить issuer в браузере и в s_client См. ERR_CERT_AUTHORITY_INVALID
«Не удаётся проверить сертификат сервера» в почтовом клиенте или FTP Обычно та же неполная цепочка на нестандартном порту openssl s_client -connect host:993 -servername host См. не удаётся проверить сертификат сервера

Чего правильная цепочка НЕ чинит

Полезно заранее знать границы: цепочка отвечает только за построение пути доверия. Она не поможет, если:

  • Истёк листовой сертификат. Путь построится, проверка сроков всё равно провалится — см. истёкший сертификат.
  • Имя не совпадает. Сертификат на example.com не подойдёт для api.example.com, если этого имени нет в SAN. Разница между типами покрытия — в материале про типы SSL-сертификатов.
  • Сертификат отозван. Отзыв проверяется отдельно, через OCSP или CRL.
  • Сертификат самоподписанный. Цепочки нет в принципе — см. самоподписанные сертификаты.
  • Корня УЦ нет у клиента. Никакая правильная сборка файла не заставит клиента доверять неизвестному якорю.
  • Не совпадают версии TLS или наборы шифров. Соединение не дойдёт до обмена сертификатами — см. разбор TLS-хендшейка.
  • Сбиты часы на клиенте. Устройство с датой из будущего или прошлого сочтёт валидный сертификат просроченным или ещё не действующим.
  • Не настроен SNI. Сервер отдаст чужой сертификат по умолчанию, и вы будете чинить цепочку не того сайта.

Как проверить цепочку в enterno.io

Если не хочется собирать вывод openssl руками или нужно показать результат коллеге:

  • SSL-чекер — проверяет сертификат из внешнего окружения, без ваших локальных кешей и корпоративных корней, и рисует дерево цепочки: какой сертификат кем подписан и где обрыв. Это самый быстрый способ ответить на вопрос «что реально отдаёт мой сервер».
  • Справочник ошибок SSL — расшифровка кодов и сообщений, если вы получили ошибку и не понимаете, к какому слою она относится.
  • Сканер безопасности — смотрит на HTTPS и заголовки целиком, а не только на сертификат: полезно, когда «что-то не так с сайтом», но что именно — неясно.
  • Мониторинг — периодическая проверка сертификата и цепочки с оповещением. Про то, какие параметры вообще имеет смысл мониторить, есть отдельный материал: мониторинг SSL-сертификатов.
  • Если нужно разобрать сам сертификат — поля, SAN, сроки, издателя — это проверка сертификата сайта.
Панель проверки SSL с деревом цепочки сертификатов и отметкой обрыва
Визуализация цепочки показывает разрыв нагляднее, чем текстовый вывод консольной утилиты.

Автоматизация: не дать цепочке развалиться снова

Цепочка ломается не в момент настройки, а через месяцы — при продлении, миграции или смене промежуточного у УЦ. Поэтому одноразовая починка бесполезна без проверки.

  • Постпроверка в хуке продления. ACME-клиенты умеют выполнять команду после успешного обновления. Добавьте туда проверку количества сертификатов и openssl verify — падение хука заметнее, чем тихо сломанный сайт.
  • Проверка в CI. Перед выкладкой конфига прогоняйте openssl verify -untrusted chain.pem cert.pem. Стоит секунду, ловит целый класс инцидентов.
  • Smoke-тест из чистого контейнера. После деплоя дёрните сайт из свежего образа без -k и без добавленных корней.
  • Внешний мониторинг. Проверка изнутри вашей сети врёт: там могут стоять корпоративные корни и прокси. Нужен взгляд снаружи.
  • Инвентарь точек терминации. Часто про основной домен помнят, а про почту, API-поддомен, вебхук-эндпоинт и старый балансировщик — нет. Сломается именно то, про что забыли.
#!/bin/sh
# Минимальная постпроверка после продления. Ненулевой код — сигнал.
HOST="example.com"
N=$(openssl s_client -showcerts -connect "$HOST:443" -servername "$HOST" </dev/null 2>/dev/null \
    | grep -c "BEGIN CERTIFICATE")
if [ "$N" -lt 2 ]; then
  echo "chain too short for $HOST: $N cert(s)" >&2
  exit 1
fi
openssl s_client -connect "$HOST:443" -servername "$HOST" </dev/null 2>/dev/null \
  | grep -q "Verify return code: 0" || exit 1

Не превращайте отключение проверки в решение. Флаги -k у curl, verify=False в Python, rejectUnauthorized в Node и импорт корня в cacerts «чтобы заработало» гасят симптом и оставляют вас без защиты от подмены трафика ровно там, где она нужна.

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

Нужно ли включать корневой сертификат в fullchain.pem?

Нет. Клиент уже имеет корень в своём хранилище, а присланной копии он всё равно не поверит «просто так». Лишний корень только увеличивает объём хендшейка. Единственное исключение — закрытые контуры, где вы сами раздаёте корень и точно знаете, что делаете.

Сколько промежуточных сертификатов должно быть в цепочке?

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

Почему Chrome открывает сайт, а Python requests — нет?

Chrome умеет догружать промежуточный по AIA и держит кеш ранее виденных сертификатов. Python requests использует бандл certifi, где лежат только корни, и никуда не ходит. Итог: браузер маскирует неполную цепочку, скрипт — нет. Чинить надо сервер, а не скрипт.

Может ли цепочка сломаться сама, без изменений на сервере?

Да, и это не редкость. Истёк промежуточный, УЦ ввёл новый промежуточный, истёк корень, к которому вёл кросс-подписанный путь, клиент обновил CA-бандл и выкинул старый корень. Сервер при этом не меняли ни разу. Ровно поэтому нужен внешний мониторинг, а не «проверили один раз при настройке».

Что означает «unable to get local issuer certificate»?

Дословно: клиент не нашёл у себя сертификат издателя для того сертификата, на котором остановился. Практически это два разных случая. Если сервер прислал один сертификат — не хватает промежуточного на сервере. Если прислал два и больше — не хватает корня в хранилище клиента. Различить помогает подсчёт присланных сертификатов.

Влияет ли порядок сертификатов в файле на что-то реально?

Да. Листовой обязан быть первым — это требование протокола во всех версиях TLS. Порядок остальных современные клиенты обычно разбирают, но старые стеки и часть библиотек — нет. Собирать в правильном порядке дешевле, чем потом искать, у кого именно из партнёров «иногда не работает».

Как быстро проверить цепочку с мобильного устройства?

С самого устройства — неудобно и ненадёжно: браузер может пользоваться кешем и AIA. Быстрее и достовернее проверить внешним сервисом, который смотрит на сервер из чистого окружения. SSL-чекер enterno.io показывает ровно то, что отдаёт сервер, без ваших локальных корней.

Чеклист

  • Сервер отдаёт минимум два сертификата: лист и промежуточный — проверено grep -c "BEGIN CERTIFICATE".
  • Листовой сертификат идёт первым в файле.
  • Issuer каждого сертификата совпадает с subject следующего.
  • Корневой сертификат в файл не включён.
  • Verify return code: 0 (ok) с чистой машины, а не только с рабочего ноутбука.
  • В конфиге указан fullchain.pem, а не cert.pem.
  • Проверка сделана с -servername, то есть с корректным SNI.
  • Проверены все точки терминации TLS: основной домен, поддомены, API, почта, вебхуки, балансировщик и CDN.
  • Проверка выполнена тем классом клиента, который реально ходит на сервер: curl, Java, мобильное приложение.
  • Сроки действия проверены у всех элементов цепочки, а не только у листа.
  • В хук продления добавлена постпроверка цепочки.
  • Настроен внешний мониторинг цепочки и срока действия.
  • Нигде в проде не осталось -k, verify=False и прочих обходов проверки.

Построение и проверка пути сертификации — RFC 5280, §6. Формат сообщения Certificate в TLS 1.3 — RFC 8446, §4.4.2. Актуальные цепочки Let's Encrypt — letsencrypt.org/certificates.

Проверьте ваш сайт прямо сейчас

Проверить SSL своего сайта →
Другие статьи: SSL/TLS
SSL/TLS
Сертификат Минцифры: как установить и вернуть доступ к банкам
31.08.2026 · 3 394 просм.
SSL/TLS
SSL Handshake Failed: причины ошибки и пошаговая диагностика
15.04.2026 · 2 134 просм.
SSL/TLS
ERR_CERT_AUTHORITY_INVALID: причины и как исправить
13.07.2026 · 1 101 просм.
SSL/TLS
Как проверить SSL-сертификат сайта — 3 способа за 2 минуты
12.04.2026 · 798 просм.