Короткий ответ

Браузеры с полным кэшем корней могут открыть 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 10expired
error 62имя не совпало

Возможные причины

  1. В nginx указано .../cert.pem вместо fullchain.pem.
  2. Ручная склейка: только leaf в /etc/nginx/ssl/site.example.crt.
  3. Apache SSLCertificateFile = leaf, забыт chain (на 2.4.8+ chain должен быть в том же файле).
  4. NPM: «Certificate» без chain в UI.
  5. После смены intermediate Let's Encrypt старый chain.pem скопирован руками и не обновляется.
  6. 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/ | head

curl без -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, корень не включать.