Короткий ответ
Браузеры с полным кэшем корней могут открыть site.example, а curl, Java, Android и старые клиенты — нет: сервер отдал только leaf без intermediate. В nginx ssl_certificate должен быть fullchain.pem (leaf + intermediate), не cert.pem. Ключ — privkey.pem. Хост 10.0.20.40, файлы /etc/letsencrypt/live/site.example/.
Проверка: openssl s_client -showcerts — две (или больше) PEM-блока до корня, который в клиенте. Корень в ssl_certificate класть не нужно и вредно смешивать.
Клиенты с кэшем AIA (Chrome) часто молчат, curl и Java — нет. Лечите серверную цепочку, не «поставьте другой браузер». На Ubuntu сравнивайте fullchain.pem с тем, что реально в nginx -T.
Симптомы и как отличить
- Chrome OK,
curlна Ubuntu:unable to get local issuer certificate. - Приложение на телефоне не коннектится, ПК да.
- SSL Labs: «Chain issues: Incomplete».
- Expired — другие даты: просрочен.
| openssl verify | Смысл |
|---|---|
| error 20 at 0 | нет issuer (intermediate) |
| error 10 | expired |
| error 62 | имя не совпало |
Возможные причины
- В nginx указано
.../cert.pemвместоfullchain.pem. - Ручная склейка: только leaf в
/etc/nginx/ssl/site.example.crt. - Apache
SSLCertificateFile= leaf, забыт chain (на 2.4.8+ chain должен быть в том же файле). - NPM: «Certificate» без chain в UI.
- После смены intermediate Let's Encrypt старый
chain.pemскопирован руками и не обновляется. ssl_trusted_certificateперепутан сssl_certificate(OCSP staple vs то, что отдаём клиенту).
Диагностика
1. Сколько сертификатов в handshake
echo | openssl s_client -connect 10.0.20.40:443 -servername site.example -showcerts 2>/dev/null | grep -E 's:|i:|BEGIN CERT'Ожидание: минимум два BEGIN CERTIFICATE (leaf, intermediate). Один BEGIN — проблема.
2. Что на диске
sudo ls -l /etc/letsencrypt/live/site.example/
sudo openssl x509 -in /etc/letsencrypt/live/site.example/cert.pem -noout -subject -issuer
sudo openssl x509 -in /etc/letsencrypt/live/site.example/chain.pem -noout -subject -issuer
sudo grep ssl_certificate /etc/nginx/sites-enabled/*cert.pem — leaf. chain.pem — intermediate. fullchain.pem — оба.
3. Verify как у строгого клиента
echo | openssl s_client -connect 10.0.20.40:443 -servername site.example 2>/dev/null | openssl x509 -noout -issuer -subject
curl -sI --resolve site.example:443:10.0.20.40 https://site.example/ | headcurl без -k должен пройти, если цепочка полная и дата ок.
4. Apache
sudo grep -n SSLCertificate /etc/apache2/sites-enabled/*
apache2ctl -vРешение
Сценарий A. Nginx + certbot
ssl_certificate /etc/letsencrypt/live/site.example/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/site.example/privkey.pem;sudo nginx -t && sudo systemctl reload nginxНе используйте cert.pem в ssl_certificate.
OCSP (опционально, nginx 1.24+):
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/site.example/chain.pem;
resolver 10.0.20.1;ssl_trusted_certificate — для staple, не замена fullchain в ssl_certificate. resolver — реальный DNS вашей сети, не выдуманный публичный как единственный, если политика запрещает.
Сценарий B. Ручной коммерческий сертификат
Склейте leaf + intermediate в одном файле в правильном порядке: сначала ваш сертификат, потом intermediate.
sudo sh -c 'cat /root/site.example.crt /root/intermediate.crt > /etc/nginx/ssl/site.example.fullchain.crt'Порядок: leaf сверху. Перепутать — снова incomplete или «self signed in chain».
Сценарий C. Apache 2.4
SSLCertificateFile /etc/letsencrypt/live/site.example/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/site.example/privkey.pemНе добавляйте корень ISRG в этот файл.
Сценарий D. Nginx Proxy Manager
В UI: сертификат + цепочка в поле chain, или Let's Encrypt внутри NPM, который кладёт fullchain сам. Если проксирует на origin с неправильной цепочкой и «SSL on» между NPM и бэкендом — чините origin тоже. См. NPM.
Сценарий E. Устаревшая копия chain
Удалите самодельные копии из vhost, укажите live/fullchain.pem, чтобы certbot renew обновлял то, что отдаётся. Хук reload после renew.
Сценарий F. Смена intermediate Let's Encrypt
Когда CA меняет промежуточный сертификат, live/fullchain.pem после обычного renew обновляется. Если вы склеили leaf вручную год назад в /etc/nginx/ssl/site.crt и забыли, цепочка протухнет при живом leaf. Симптом: «вчера Android открывал, после renew — нет» при том что notAfter leaf в будущем. Переведите vhost на fullchain.pem и удалите ручную склейку из процесса деплоя.
Проверка порядка PEM:
sudo awk 'BEGIN{n=0} /BEGIN CERT/{n++; print "cert",n} /SUBJECT|Issuer/=0' /etc/letsencrypt/live/site.example/fullchain.pem
sudo openssl crl2pkcs7 -nocrl -certfile /etc/letsencrypt/live/site.example/fullchain.pem 2>/dev/null | openssl pkcs7 -print_certs -nooutПервый сертификат должен быть для site.example, второй — intermediate (issuer корня). Если первый — R3/E5/E6 без имени сайта, файл перевёрнут.
На Apache не смешивайте SSLCertificateChainFile со старым leaf в SSLCertificateFile, если уже положили fullchain в File: получите дубль intermediate.
Как проверить, что проблема устранена
echo | openssl s_client -connect 10.0.20.40:443 -servername site.example -showcerts 2>/dev/null | grep 'BEGIN CERT' | wc -l
curl -sI --resolve site.example:443:10.0.20.40 https://site.example/ >/dev/null && echo curl_okДва или больше PEM в handshake, curl без -k успешен. Повторите с клиента, который раньше падал (Java keytool не обязателен: достаточно curl на чистой Ubuntu).
Если не помогло
- CDN подменяет cert и отдаёт свой chain — смотрите edge, не origin.
- Старый Android и SHA-1 intermediate — обновите цепочку CA, не отключайте проверку в приложении.
ssl_certificateдублируется вhttpиserver— выигрывает server, проверьте оба.- Mixed content после починки HTTPS — mixed.
curlна Ubuntu проходит, Java-клиент нет: у Java свой cacerts и строже к порядку chain. Сверьтеshowcertsещё раз: leaf должен быть первым PEM, не intermediate.
Профилактика
- Только
fullchain.pemв шаблоне vhost. - После смены CA — проверка
showcerts. - Не хранить «урезанный» crt рядом как основной.
FAQ
Нужно ли отдавать корневой ISRG Root X1?
Обычно нет. Клиент должен иметь корень. Отдача корня не заменяет intermediate.
chain.pem vs fullchain.pem
chain без leaf. Nginx в ssl_certificate нужен fullchain.
Почему Chrome молчал?
Он подтянул intermediate через AIA caIssuers. Строгие клиенты не обязаны.
Можно ли ssl_prefer_server_ciphers вместо chain?
Нет, это шифры, не PKI.
Два intermediate
Бывает. Все промежуточные — в fullchain после leaf, корень не включать.