1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6

# теория · шаг 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 - листай до «Теперь сам».

Сначала ответь сам

/etc/nginx/conf.d/shop.local.confhttp server
ssl_certificate     /etc/letsencrypt/live/shop.local/cert.pem;
ssl_certificate_key /etc/letsencrypt/live/shop.local/privkey.pem;

Файлы на месте, nginx -t доволен, сайт в браузере открывается без предупреждений. Что сломано?

Указан cert.pem - только сертификат сервера, без промежуточного. Стенд показывает разницу сразу:

↳  выводcurl --cacert root.crt https://shop.local:8443/
curl: (60) SSL certificate problem: unable to get local issuer certificate

Браузеры на десктопе умеют дотягивать недостающее звено сами, а curl, мобильные приложения, Java-клиенты и платёжные шлюзы - нет. Симптом «у нас всё открывается, а платёжка не подключается» почти всегда про эту строку.

На что это похоже

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

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

Ровно это и лежит в fullchain.pem: твой сертификат плюс промежуточный. Корневой добавлять не нужно, он есть в хранилище клиента.

Механизм: цепочка из трёх звеньев

сертификат сайта CN=shop.local промежуточный выдал сертификат сайта корневой лежит у клиента это и есть fullchain.pem - его отдаёт сервер cert.pem - только левый прямоугольник chain.pem - только средний privkey.pem - ключ, он никуда не уходит
Порядок в файле обязателен: сначала сертификат сайта, потом промежуточные.

Правило

/etc/nginx/conf.d/shop.local.confhttp server
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, а подсказка, что за файл):

↳  выводчто лежит в /etc/letsencrypt/live/shop.local/
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'
↳  вывод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'
↳  вывод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
↳  вывод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 отказывается применять конфиг:

↳  выводnginx -t
[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, и она работает сама:

/etc/nginx/conf.d/shop.local.confhttp server
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: сертификат живёт в памяти процесса.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.