С чего начать Первые шаги трек · 15 ступеней
Linux Labs

TLS и сертификаты — openssl, цепочка, проверка

10 мин Ubuntu 24.04 · OpenSSL 3.0 · curl 8.5

TLS-сертификат связывает ключ сервера с именем сайта; openssl читает его поля, проверяет цепочку и выпускает новый.

Шпаргалка

9 команд Версия для печати
  • openssl x509 -in site.pem -noout -subject -issuer -dates
  • openssl x509 -in site.pem -noout -ext subjectAltName имена
  • openssl x509 -in site.pem -noout -checkend 2592000 30 дней?
  • openssl s_client -connect site.lab.test:443 -servername site.lab.test \
  • -showcerts </dev/null 2>/dev/null | grep -E '^ *[0-9]+ s:|^ +i:'
  • openssl verify -CAfile ca.pem -untrusted int.pem site.pem
  • curl --cacert ca.pem https://site.lab.test/ свой CA
  • sudo cp ca.pem /usr/local/share/ca-certificates/lab.crt
  • sudo update-ca-certificates CA в доверие

Обзор

TLS шифрует соединение и до первого байта http даёт клиенту проверить, что сервер — тот, за кого себя выдаёт. Для этого сервер показывает сертификат: открытый ключ, имена сайта и срок, заверенные подписью центра сертификации. Парный закрытый ключ лежит только на сервере.

text
$ openssl x509 -in site.pem -noout -subject -issuer -dates \
    -ext subjectAltName
subject=CN = secure.lab.test
issuer=CN = Lab Intermediate CA
notBefore=Oct 20 10:04:50 2026 GMT
notAfter=Nov 19 10:04:50 2026 GMT
X509v3 Subject Alternative Name:
    DNS:secure.lab.test

subject — кому выдан, issuer — кто подписал, notBefore/notAfter — срок, SAN — имена, для которых сертификат годен.

Три проверки клиента

Проверка Что сверяет Ошибка curl
доверие цепочка подписей доходит до CA из хранилища unable to get local issuer certificate
имя имя из адреса есть в SAN no alternative certificate subject name matches
срок сейчас между notBefore и notAfter certificate has expired

Любая не прошла — curl выходит с кодом 60, браузер показывает страницу ошибки. curl -k отключает все три: соединение шифруется, но с кем — неизвестно.

Цепочка и доверие

Сертификат сайта обычно подписан не корнем, а промежуточным CA, а тот — корневым. Корень подписан сам собой (subject = issuer), и ему верят потому, что он лежит в хранилище доверия клиента. В Ubuntu это /etc/ssl/certs; свой корень туда добавляет update-ca-certificates, для одного запроса — curl --cacert ca.pem.

Клиент знает только корни. Промежуточный ему присылает сервер, вместе с сертификатом сайта — это цепочка. Сервер отдал один сертификат сайта — клиент не может дойти до корня и пишет unable to get local issuer certificate, хотя корень у него есть.

Ключ, запрос, сертификат

Файлы PEM — текст между строками -----BEGIN …----- и -----END …-----, внутри base64. По первой строке видно, что в файле:

Первая строка Что это
-----BEGIN PRIVATE KEY----- закрытый ключ
-----BEGIN CERTIFICATE REQUEST----- запрос CSR
-----BEGIN CERTIFICATE----- сертификат

Сертификат выпускают из запроса (CSR): в нём открытый ключ и нужные имена. CA подписывает запрос и возвращает сертификат. Новый запрос на прежний ключ даёт новый сертификат без смены ключа.

Рецепты

8

Что отдаёт сервер

bash
openssl s_client -connect secure.lab.test:443 \
  -servername secure.lab.test -showcerts </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -issuer -enddate

Первый сертификат в выводе — сертификат сайта. Цепочку целиком — строки s: (кому) и i: (кем подписан):

text
 0 s:CN = secure.lab.test
   i:CN = Lab Intermediate CA
 1 s:CN = Lab Intermediate CA
   i:CN = Lab Root CA

Только строка 0 — сервер не отдаёт промежуточный. -servername передаёт имя сайта в начале TLS (SNI): по нему nginx с несколькими сайтами на 443 выбирает сертификат. Без имени, например при подключении к IP, приходит сертификат сервера по умолчанию.

Проверить цепочку локально

bash
openssl verify -CAfile ca.pem site.pem
openssl verify -CAfile ca.pem -untrusted int.pem site.pem

Первая команда без промежуточного — error 20 at 0 depth lookup: unable to get local issuer certificate. Вторая — site.pem: OK: от сайта через промежуточный до корня цепочка сходится.

Собрать файл полной цепочки

bash
cat site.pem int.pem > fullchain.pem
grep -c BEGIN fullchain.pem      # 2

Порядок — сертификат сайта первым, за ним промежуточный. Корень в файл не кладут: он у клиента свой. Подключение файла в nginx — nginx.

Ключ и сертификат — пара?

bash
openssl x509 -in site.pem -noout -pubkey | sha256sum
openssl pkey -in site.key -pubout | sha256sum

Суммы совпали — пара. Не совпали — nginx не примет конфиг: key values mismatch.

Выпустить сертификат с прежним ключом

bash
openssl req -new -key site.key -out site.csr -subj '/CN=shop.lab.test' \
  -addext 'subjectAltName=DNS:shop.lab.test,DNS:www.shop.lab.test'
openssl x509 -req -in site.csr -CA ca.pem -CAkey ca.key \
  -CAcreateserial -days 90 -copy_extensions copy -out site.pem
openssl x509 -in site.pem -noout -ext subjectAltName -enddate

-copy_extensions copy переносит SAN из запроса в сертификат. Без него сертификат выйдет без имён: -ext subjectAltName напишет No extensions in certificate, и curl отвергнет его по имени. Готовый сертификат — в файл, на который смотрит сервер, затем nginx -t и reload.

Сколько осталось до конца срока

bash
openssl x509 -in site.pem -noout -enddate
openssl x509 -in site.pem -noout -checkend 2592000; echo $?

-checkend N — истечёт ли сертификат за N секунд (2592000 — 30 дней): Certificate will expire и код 1 или Certificate will not expire и код 0. Удобно в проверках и мониторинге.

Свой CA в доверие системы

bash
sudo cp ca.pem /usr/local/share/ca-certificates/lab-root.crt
sudo update-ca-certificates

Файл должен называться *.crt. Команда пишет 1 added, 0 removed; done., после этого curl и другие программы на OpenSSL верят сертификатам этого CA без --cacert.

Let's Encrypt (certbot)

bash
sudo certbot --nginx -d example.com -d www.example.com
sudo certbot renew --dry-run
systemctl list-timers certbot.timer

Публичный CA для сайтов в интернете: сертификат на 90 дней, таймер продлевает его заранее. Нужны DNS-имя на этот сервер и доступный снаружи порт 80. В учебных ВМ сети наружу нет, поэтому там — свой учебный CA.

Неполадки

6

certificate has expired

curl: (60) SSL certificate problem: certificate has expired, у openssl s_client — Verify return code: 10. Проверь notAfter сертификата, который отдаёт сервер (рецепт выше), и часы машины — timedatectl. Выпусти новый, замени файл и сделай reload: старый сертификат nginx держит в памяти.

unable to get local issuer certificate

Клиент не дошёл до доверенного корня. Две причины:

  • CA клиенту не известен — --cacert или update-ca-certificates;
  • сервер не отдаёт промежуточный — у s_client одна строка 0 s: и Verify return code: 21 (unable to verify the first certificate). Нужен файл полной цепочки.

no alternative certificate subject name matches

curl: (60) SSL: no alternative certificate subject name matches target host name 'other.lab.test' — имени из адреса нет в SAN. Так же curl ответит на обращение по IP. Посмотри имена: openssl x509 -noout -ext subjectAltName; если SAN пуст — сертификат подписан без -copy_extensions.

self-signed certificate

curl: (60) SSL certificate problem: self-signed certificate, у s_client — код 18. Сервер отдаёт сертификат, подписанный им самим, и в хранилище доверия его нет. Для своего сервиса — выпусти сертификат от своего CA и положи корень в доверие.

wrong version number

curl: (35) … SSL routines::wrong version number — https:// ушёл на порт, где говорят обычным HTTP. Проверь порт и listen … ssl у сервера.

Сервер отдаёт старый сертификат

Файл заменён, а s_client показывает старый notAfter: nginx не перечитал конфиг. sudo nginx -t && sudo systemctl reload nginx и проверка через секунду — nginx.

Опасно

2

curl -k в скриптах и проверках

-k (--insecure) молча пропускает подмену сервера, истёкший сертификат и чужое имя. Проверка «сайт отвечает по HTTPS» с -k проходит и тогда, когда пользователи видят ошибку. Вместо -k — --cacert с нужным CA.

Закрытый ключ

Ключ даёт право выдавать себя за сайт. Права — 600, владелец — root, в репозитории, письма и чаты ключ не попадает. Утёк — выпусти сертификат на новый ключ.

Термины

11
TLS
Протокол шифрованного соединения поверх TCP: клиент проверяет сертификат сервера, потом обе стороны шифруют обмен. HTTPS — это HTTP внутри TLS. Ещё встречается в: curl, HTTP, wget, nginx и ещё 4
сертификат (X.509) certificate (X.509)
Файл, где удостоверяющий центр своей подписью связывает открытый ключ с именами сайта на срок от notBefore до notAfter. Сервер показывает его клиенту в начале TLS. Ещё встречается в: curl, nginx, wget, GPG и ещё 3
центр сертификации (CA) certificate authority (CA)
Тот, кто подписывает сертификаты; его имя — в поле issuer. Клиент верит сертификату, если цепочка подписей доходит до корневого CA из его хранилища доверия. Ещё встречается в: wget, curl
цепочка сертификатов certificate chain
Сертификат сайта и промежуточные сертификаты CA до корня, каждый подписан следующим. Сервер отдаёт сертификат сайта и промежуточные, корень у клиента свой. Ещё встречается в: nginx, wget
закрытый ключ private key
Секретная половина пары ключей; лежит на сервере и доказывает, что сертификат принадлежит ему. Сертификат и ключ должны быть парой, иначе nginx не примет конфиг (key values mismatch).
запрос на сертификат (CSR) certificate signing request (CSR)
Файл с открытым ключом и нужными именами, подписанный закрытым ключом. CA выпускает по нему сертификат; новый CSR на прежний ключ — новый сертификат без смены ключа.
SAN (альтернативные имена) SAN (subject alternative name)
Расширение сертификата со списком имён, для которых он годен; клиент сверяет с ним имя из адреса. Имени нет в SAN — ошибка «no alternative certificate subject name matches».
хранилище доверия trust store
Корневые сертификаты CA, которым верит клиент. В Ubuntu — /etc/ssl/certs, свой CA туда добавляет update-ca-certificates; для одного запроса CA указывают ключом curl --cacert.
самоподписанный сертификат self-signed certificate
Сертификат, подписанный своим же ключом: subject и issuer совпадают. Так устроен любой корневой CA — ему верят, потому что он в хранилище доверия; чужой такой сертификат curl отвергает (self-signed certificate). Ещё встречается в: curl
промежуточный CA intermediate CA
Центр сертификации между корнем и сайтом — его подписал корневой CA, а он подписывает сертификаты сайтов. Клиент его не знает, поэтому сервер отдаёт его вместе с сертификатом сайта.
SNI
Имя сайта, которое клиент сообщает в самом начале TLS, ещё до HTTP. По нему сервер с несколькими сайтами на одном адресе выбирает, какой сертификат отдать. Ещё встречается в: curl

См. также

  • nginx — ssl_certificate, цепочка, перенаправление на HTTPS.
  • http — что идёт внутри TLS, коды ответа.
  • curl — --cacert, -v с разбором рукопожатия.
  • timedatectl — часы машины, если «истёк» слишком рано.
  • sshd — другая служба на ключах и отпечатках.
Загрузка…