# теория · шаг 2 из 6
Что лежит в двух файлах
Коротко
TLS в nginx включается двумя строками, и почти все беды с ним - это беды с содержимым двух файлов.
ssl_certificate- сертификат сервера плюс промежуточные, в одном файле (fullchain.pem, а неcert.pem). Ключ - отдельно, права600(читать и писать только владельцу, остальным - ничего).- Без промежуточного curl отвечает
unable to get local issuer certificate (20), а браузер на десктопе часто открывает сайт: отсюда «у меня работает, а интеграция не подключается». - Имена берутся из SAN. Сертификат, где имя только в CN, curl принимает, а
Chrome отвергает с
ERR_CERT_COMMON_NAME_INVALID. - nginx держит сертификат в памяти с момента загрузки конфига: заменил файлы -
нужен
reload.
Если знаешь разницу fullchain.pem и cert.pem и помнишь про SAN - листай до «Теперь сам».
Сначала ответь сам
ssl_certificate /etc/letsencrypt/live/shop.local/cert.pem;
ssl_certificate_key /etc/letsencrypt/live/shop.local/privkey.pem;
Файлы на месте, nginx -t доволен, сайт в браузере открывается без
предупреждений. Что сломано?
Указан cert.pem - только сертификат сервера, без промежуточного. Стенд
показывает разницу сразу:
curl: (60) SSL certificate problem: unable to get local issuer certificate
Браузеры на десктопе умеют дотягивать недостающее звено сами, а curl, мобильные приложения, Java-клиенты и платёжные шлюзы - нет. Симптом «у нас всё открывается, а платёжка не подключается» почти всегда про эту строку.
На что это похоже
Пропуск в здание, подписанный начальником отдела. Охранник начальника отдела не знает, зато знает директора - и хочет видеть бумагу, где директор подтверждает полномочия начальника.
Ты приносишь обе бумаги сразу: свой пропуск и подтверждение полномочий. Приказ о назначении самого директора приносить не надо - он у охранника в папке.
Ровно это и лежит в fullchain.pem: твой сертификат плюс промежуточный.
Корневой добавлять не нужно, он есть в хранилище клиента.
Механизм: цепочка из трёх звеньев
Правило
listen 443 ssl;
server_name shop.local;
ssl_certificate /etc/letsencrypt/live/shop.local/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/shop.local/privkey.pem;
Что кладёт certbot в каталог сертификата (пояснения справа - не часть вывода
ls, а подсказка, что за файл):
cert.pem сертификат сайта
chain.pem промежуточные
fullchain.pem cert.pem + chain.pem ← в ssl_certificate
privkey.pem приватный ключ ← в ssl_certificate_key
Разбор: как читать вывод s_client
-CAfile root.crt даёт openssl корневой сертификат для проверки, grep оставляет
строки цепочки и итог. Правильная цепочка (сервер с fullchain.pem):
openssl s_client -connect shop.local:443 -servername shop.local -CAfile root.crt </dev/null 2>/dev/null | grep -E 's:|Verify return'
0 s:CN = shop.local
1 s:CN = Lab Intermediate CA
Verify return code: 0 (ok)
Тот же запрос к серверу, отдающему только лист (cert.pem):
openssl s_client -connect shop.local:8443 -servername shop.local -CAfile root.crt </dev/null 2>/dev/null | grep -E 's:|Verify return'
0 s:CN = shop.local
Verify return code: 21 (unable to verify the first certificate)
Список обрывается на нулевом уровне - промежуточного в файле нет. Verify return
code: 21 у openssl и (60) ... unable to get local issuer certificate у curl
означают одно и то же: клиент дошёл до звена, чей издатель ему неизвестен (число
21 - это внутренний код проверки X.509, 60 - код выхода самого curl).
Параметр -servername обязателен: без него openssl не отправит имя в
рукопожатии, и сервер ответит сертификатом блока по умолчанию. Половина
сообщений «сервер отдаёт чужой сертификат» - это забытый параметр; почему так,
разбирает следующий урок.
Разбор: имя живёт в SAN, а не в CN
openssl x509 -in fullchain.pem -noout -dates -ext subjectAltName
notBefore=Aug 1 09:12:44 2026 GMT
notAfter=Oct 30 09:12:43 2026 GMT
X509v3 Subject Alternative Name:
DNS:shop.local, DNS:www.shop.local
Проверка на стенде: выписан сертификат, у которого имя cnonly.local стоит
только в CN, никакого SAN нет. Один и тот же сертификат, два клиента:
| клиент | результат |
|---|---|
| curl | 200, ошибок нет (проверено: SSL certificate verify ok) |
| Chrome (корень в хранилище) | ERR_CERT_COMMON_NAME_INVALID |
OpenSSL до сих пор смотрит на CN, когда SAN отсутствует вовсе; браузеры этот запасной путь убрали. Практический вывод один: смотри SAN. Если имени нет там - неважно, что написано в CN.
Что ломается без этого
Ключ не от того сертификата. Файлы перепутали при копировании, и nginx отказывается применять конфиг:
[emerg] SSL_CTX_use_PrivateKey("/certs/shop.key") failed
(SSL: error:05800074:x509 certificate routines::key values mismatch)
Хорошая новость: пока конфиг не проходит проверку, reload не применяется, и
сайт продолжает работать со старым сертификатом. Плохая - если ты этого не
заметил, ты уверен, что новый сертификат уже стоит. Проверяется до перезагрузки
сравнением модулей:
openssl x509 -noout -modulus -in fullchain.pem | openssl md5
openssl rsa -noout -modulus -in privkey.pem | openssl md5
Сертификат живёт в памяти. Замер: подменили оба файла - сервер продолжает
отдавать старый серийный номер; сделали reload - отдаёт новый. Отсюда и
--deploy-hook у certbot, о котором в следующей главе: без перезагрузки
продлённый сертификат до клиентов не доедет.
Зачем это в работе
Две пары ключей в одном блоке - штатная возможность с версии 1.11, и она работает сама:
ssl_certificate /certs/rsa.fullchain.crt;
ssl_certificate_key /certs/rsa.key;
ssl_certificate /certs/ec.fullchain.crt;
ssl_certificate_key /certs/ec.key;
Стенд: клиент, умеющий ECDSA, получает подпись ecdsa_secp256r1_sha256, клиент
без неё - rsa_pss_rsae_sha256. Каждому достаётся тот сертификат, который он
понимает, и ни одной дополнительной строки для этого не нужно.
Теперь сам
Мониторинг говорит, что сертификат протух, хотя certbot три дня назад отчитался
об успешном продлении. openssl x509 -in fullchain.pem -noout -enddate на диске
показывает свежую дату. Где искать?
В памяти nginx. Файл продлён, а процесс держит тот сертификат, который прочитал
при последней загрузке конфига, - это и показал замер выше. Не сработал
--deploy-hook, то есть после продления никто не сделал reload. Проверяется
одной командой: сравни дату из файла с датой, которую отдаёт
openssl s_client -servername shop.local живьём.
Главное
ssl_certificate - это сертификат сайта плюс промежуточные в одном файле
(fullchain.pem, не cert.pem), ssl_certificate_key - ключ с правами 600.
Без промежуточного браузер на десктопе часто открывает сайт, а curl отвечает
unable to get local issuer certificate - отсюда «у нас работает, у них нет».
Имена берутся из SAN: сертификат с именем только в CN curl принимает, а Chrome
отвергает. Пару проверяют сравнением модулей, цепочку - openssl s_client
-servername, а после замены файлов делают reload: сертификат живёт в памяти
процесса.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий