Коротко. Цепочка сертификатов — это путь от сертификата вашего сайта до корневого центра сертификации, которому доверяет клиент. Сервер обязан отдать листовой сертификат и все промежуточные; корневой клиент берёт из собственного хранилища доверия. Если промежуточного нет, десктопный браузер часто выкрутится сам, а 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 Roots | Keychain Access; security find-certificate -a -p /System/Library/Keychains/SystemRootCertificates.keychain |
| Debian / Ubuntu | Пакет ca-certificates, собранный бандл /etc/ssl/certs/ca-certificates.crt | ls /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 году. Сервера не менялись, сертификаты были валидны, но часть клиентов начала отваливаться, а часть продолжила работать. Причины ровно три, и все три — про построение пути:
- Клиенты с ISRG Root X1 в хранилище находили короткий путь и завершали проверку на нём. Истёкший DST Root CA X3 их не касался.
- Клиенты со старым OpenSSL (ветка 1.0.2) находили длинный путь через истёкший корень, получали ошибку и не пробовали альтернативу — механизм перебора альтернативных путей появился в более поздних ветках. Формально валидный путь существовал, но библиотека до него не доходила.
- Старый 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 / GnuTLS | CA-бандл ОС | Нет | unable to get local issuer certificate, код выхода 60 |
| Java (PKIX) | cacerts внутри JDK | Нет по умолчанию; включается системным свойством com.sun.security.enableAIAcaIssuers | PKIX 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 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.
Где взять потерянный промежуточный
- AIA внутри вашего же сертификата — самый надёжный путь, потому что даёт ровно тот промежуточный, который подписал именно ваш лист:
openssl x509 -in cert.pem -noout -ext authorityInfoAccess curl -sS http://<caIssuers-url> | openssl x509 -inform DER -out intermediate.pem - Сайт удостоверяющего центра — у Let's Encrypt это letsencrypt.org/certificates, у коммерческих УЦ есть аналогичные разделы с репозиторием промежуточных.
- Логи Certificate Transparency — найдите свой сертификат на crt.sh и перейдите по ссылке на издателя.
- Переиздать сертификат — если УЦ сменил промежуточный, старый может уже не подходить. Перевыпуск через 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, сроки, издателя — это проверка сертификата сайта.

Автоматизация: не дать цепочке развалиться снова
Цепочка ломается не в момент настройки, а через месяцы — при продлении, миграции или смене промежуточного у УЦ. Поэтому одноразовая починка бесполезна без проверки.
- Постпроверка в хуке продления. 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.