CorBlogкейсы · ops

Статья

Пара сертификата: .crt, .key и корневой CA

Корневая, промежуточная и пара сайта: что в .crt и .key, и зачем лист с промежуточным склеивают в fullchain.pem.

У TLS-сертификата всегда пара: закрытый ключ и открытый сертификат. В рабочей схеме таких пар три, не одна.

Корневая пара — ca.key и ca.crt. Сертификат подписывает сам себя: issuer совпадает с subject. Открытую половину кладут в доверенные корни ОС или браузера. Закрытую не отдают: ей только подписывают то, что ниже. Кто принял ca.crt, верит всему, что этот ключ подписал.

Промежуточная пара — intermediate.key и intermediate.crt. Корень подписывает её сертификат, а её ключ подписывает сертификаты сайтов. Корень из-за этого почти не достают. Браузер промежуточный сертификат сам не хранит, поэтому сервер обязан его прислать вместе с сертификатом сайта.

Пара сайта — site.key и site.crt. Это лист: им нельзя подписывать другие сертификаты. Ключ остаётся только на веб-сервере этого имени. Сертификат подписывает промежуточный CA. В совсем маленькой лабе лист иногда подписывает сразу корень, но в бою между ними почти всегда есть промежуточный.

Полная цепочка, которую отдают серверу, — один текстовый PEM. Блоки BEGIN CERTIFICATE идут сверху вниз: сначала лист, потом промежуточный. Закрытые ключи в этот файл не входят. Корень тоже не дописывают: клиент должен уже доверять ему сам, файл цепочки доверие не создаёт. Такой файл обычно называют fullchain.pem и кладут в ssl_certificate. Рядом отдельно лежит только site.key.

fullchain.pem
  site.crt            лист, его проверяют по имени
  intermediate.crt    кто подписал лист
                      корень уже в хранилище клиента
cat site.crt intermediate.crt > fullchain.pem

Сайт в примере: site.example.com. Корень лабы: Example Lab Root CA, промежуточный: Example Lab Intermediate CA. Дальше та же схема на Linux через openssl и на Windows через AD CS.

Зачем это

Без TLS кто угодно на пути между браузером и сервером читает и подменяет трафик: пароль, cookie, ответ API. Сертификат отвечает на другой вопрос, чем шифрование само по себе: это правда тот сайт, имя которого я открыл.

Шифрование сессии и проверка имени — две разные работы. Их делает связка из открытого сертификата и закрытого ключа, плюс кто-то, кому браузер уже доверяет (корневой CA).

Люди говорят SSL. В живых браузерах это TLS 1.2 или 1.3. Файлы от этого не переименовались: их всё ещё зовут SSL-сертификатом.

Пара, а не один файл

Криптопара — два ключа, которые подходят только друг к другу:

  • закрытый — секрет, им доказывают владение и подписывают;
  • открытый — его можно показывать всем, им проверяют подпись.

Сертификат (.crt, .cer, .pem) — это не ключ. Это открытый документ: имя, открытый ключ, срок, подпись того, кто выпустил.

Закрытый ключ (.key) в сертификат не кладут. Если .key утёк — пара скомпрометирована, даже если .crt выглядит нормально.

Файл Что внутри Кому отдавать
ca.key закрытый ключ корня никому, только машине CA
ca.crt сертификат корня (открытый) в доверенные корни, не в fullchain
intermediate.key закрытый ключ промежуточного CA только этой CA, не на веб-сервер
intermediate.crt сертификат промежуточного CA в fullchain.pem, сразу после листа
site.key закрытый ключ сайта только веб-серверу этого имени
site.crt сертификат сайта первым блоком в fullchain.pem
site.csr заявка: имя + открытый ключ, ещё без подписи CA только своему CA
site.pfx сертификат и закрытый ключ в одном контейнере осторожно: это уже секрет

.pem — текстовая оболочка (-----BEGIN ...-----). И сертификат, и ключ бывают в PEM. Расширение само не говорит, секрет внутри или нет — смотри заголовок блока.

Что подписывает корень

Корень — самоподписанный сертификат: поле issuer совпадает с subject, подпись сделана его же закрытым ключом. Ему не на кого опереться выше. Доверие появляется, только когда ca.crt вручную или политикой попал в хранилище (система, браузер, curl --cacert).

Дальше подписи идут вниз, не вбок:

ca.key подписывает intermediate.crt
intermediate.key подписывает site.crt

Браузер получает лист и промежуточный, проверяет обе подписи и останавливается на корне, который уже лежит у него в доверии. Нет промежуточного в цепочке — клиент видит подпись неизвестного издателя и обрывает рукопожатие, даже если корень у него есть.

Сертификат сайта привязан к имени, не к IP. Браузер смотрит SAN (subjectAltName), старый CN почти никто не считает достаточным. Нет DNS:site.example.com в SAN — предупреждение, даже если подпись верная.

Что происходит при открытии HTTPS

Упрощённо, для TLS 1.3:

  1. Браузер говорит имя (site.example.com) и предлагает параметры.
  2. Сервер отдаёт fullchain.pem: сначала site.crt, сразу за ним intermediate.crt. Корень не шлёт.
  3. Браузер проверяет цепочку до доверенного корня, срок, что имя из адресной строки есть в SAN.
  4. Сервер доказывает, что у него есть site.key от этого сертификата: подписывает кусок рукопожатия. Закрытый ключ по сети не уходит.
  5. Обе стороны получают сессионные ключи через обмен (ECDHE). Ими шифруется трафик.

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

mTLS: сертификат ещё и у клиента

Обычный HTTPS выше проверяет только сервер. Браузер убеждается, что открыл site.example.com, а сам остаётся для TLS безымянным. Кто пришёл, решает уже приложение: пароль, cookie, SSO.

mTLS (mutual TLS) — то же рукопожатие, только сервер в ответ просит сертификат у клиента. У клиента своя пара: client.key и client.crt. Сертификат подписывает CA, которому доверяет уже сервер, не браузер толпы. Клиент подписывает кусок рукопожатия своим закрытым ключом и тем самым доказывает, что пара его. Ключ по сети опять не уходит.

Доверия тут два, и они не зеркало одного файла:

Кто проверяет Чему верит Что смотрит
Клиент корневой CA сервера имя в SAN серверного сертификата
Сервер корневой CA клиентов что лист клиента подписан этим CA, часто ещё SAN или CN как имя вызывающего

CA может быть один на обе стороны или два разных. На листе клиента в расширениях нужно clientAuth, на листе сайта — serverAuth. Перепутать их нельзя: серверный сертификат клиентом не притворится.

Зачем это, если уже есть логин. Пароль и токен можно украсть и переслать. Закрытый ключ клиента с машины не уезжает, а сервер на уровне соединения знает, какой экземпляр сервиса или какой пользовательский сертификат пришёл, до разбора HTTP. Так ходят внутренние API, ingress с клиентскими сертификатами, kube-apiserver к etcd и между control plane, service mesh. Снаружи для людей чаще остаётся обычный TLS плюс SSO. mTLS чаще закрывает трафик машина-машина, где нет браузера и формы логина.

Шифрование сессии от mTLS не меняется: его по-прежнему делает одноразовый обмен ключами. Вторая пара нужна только чтобы назвать клиента.

Практику выпуска клиентских сертификатов, проверку на nginx или в Kubernetes разберу отдельно. Здесь достаточно карты: серверная пара из этой статьи, плюс такая же пара у каждого клиента, плюс CA, которому сервер верит как издателю этих клиентов.

Linux: openssl

Нужен OpenSSL 3 (Debian 12, Ubuntu 22.04+). Каталог лабы и жёсткая маска, чтобы ключи не создались читаемыми всем:

mkdir -p ~/lab-ca && cd ~/lab-ca
umask 077

Корень: ca.key + ca.crt

openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:prime256v1 -out ca.key

openssl req -new -x509 -key ca.key -sha256 -days 3650 \
  -subj "/CN=Example Lab Root CA" \
  -addext "basicConstraints=critical,CA:TRUE" \
  -addext "keyUsage=critical,keyCertSign,cRLSign" \
  -out ca.crt

CA:TRUE и keyCertSign — это разрешение подписывать другие сертификаты. Без них файл выглядит как сертификат, но нормальный клиент не примет его как корень цепочки.

Посмотреть, кто выписал сам себя:

openssl x509 -in ca.crt -noout -subject -issuer

subject и issuer совпадают. Это и есть самоподпись.

Промежуточный: подпись корнем

openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:prime256v1 -out intermediate.key

openssl req -new -key intermediate.key -sha256 \
  -subj "/CN=Example Lab Intermediate CA" \
  -out intermediate.csr

cat > intermediate.ext <<'EOF'
basicConstraints=critical,CA:TRUE,pathlen:0
keyUsage=critical,keyCertSign,cRLSign
EOF

openssl x509 -req -in intermediate.csr \
  -CA ca.crt -CAkey ca.key -CAcreateserial \
  -sha256 -days 1825 -extfile intermediate.ext \
  -out intermediate.crt

pathlen:0 — этим промежуточным можно подписать только лист, не ещё один CA. intermediate.key на веб-сервер не кладут.

Сайт: ключ, заявка, подпись промежуточным

openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:prime256v1 -out site.key

openssl req -new -key site.key -sha256 \
  -subj "/CN=site.example.com" \
  -out site.csr

CSR ещё не сертификат: там нет подписи CA. Подпись и нужные расширения — отдельным файлом, иначе SAN легко потерять:

cat > site.ext <<'EOF'
basicConstraints=CA:FALSE
keyUsage=critical,digitalSignature
extendedKeyUsage=serverAuth
subjectAltName=DNS:site.example.com
EOF

openssl x509 -req -in site.csr \
  -CA intermediate.crt -CAkey intermediate.key -CAcreateserial \
  -sha256 -days 397 -extfile site.ext \
  -out site.crt

CA:FALSE — это лист, им нельзя подписывать других. serverAuth — сертификат для TLS-сервера, не для почты и не для подписи кода.

Проверки, которые я делаю до того, как класть файлы в nginx:

openssl verify -CAfile ca.crt -untrusted intermediate.crt site.crt
openssl x509 -in site.crt -noout -subject -issuer -ext subjectAltName

openssl verify печатает OK, только если лист сходится с промежуточным, а промежуточный — с корнем в -CAfile. -untrusted как раз и есть тот сертификат, который сервер досылает в цепочке. Имя в SAN должно быть тем, что откроет браузер.

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

cat site.crt intermediate.crt > fullchain.pem

Пара совпала, если открытый ключ из сертификата и из .key один и тот же:

openssl x509 -in site.crt -noout -pubkey | openssl sha256
openssl pkey -in site.key -pubout | openssl sha256

Два одинаковых хеша. Разные — сертификат выписан на другой ключ, сервер им не докажет владение.

В nginx это два разных параметра, не один файл:

ssl_certificate     /etc/ssl/fullchain.pem;
ssl_certificate_key /etc/ssl/site.key;

Клиенту, который не знает лабу, корень нужно отдать отдельно (curl --cacert ca.crt или импорт ca.crt в доверенные). fullchain.pem доверия не создаёт: в нём нет корня, только путь до него.

Windows: AD CS

Та же пара, только закрытый ключ корня живёт в хранилище службы Certification Authority, а не в файле ca.key на рабочем столе. Открытая половина — сертификат CA, его публикуют в домен и в Trusted Root Certification Authorities.

Ниже — корпоративный корневой CA в домене. Это лаба, не схема для интернета: такой корень доверяют только машины, куда он попал.

Роль и корень

На Windows Server в домене:

  1. Server Manager → Add Roles and Features → Active Directory Certificate Services → роль Certification Authority.
  2. Тип: Enterprise CA (нужен домен; Standalone тоже умеет подписывать, но без шаблонов и автопубликации в AD).
  3. Root CA, новый ключ: RSA 2048 или ECDSA P-256, хеш SHA256.
  4. Имя, например Example Lab Root CA. Срок корня длинный (годы), срок сертификатов сайтов — около года.

После установки открытая половина уже в AD. Забрать файл, аналог ca.crt:

certutil -ca.cert C:\ca.crt

В certlm.msc → Trusted Root Certification Authorities этот сертификат появляется у членов домена через групповую политику. Это и есть шаг «положить корень в доверие». Закрытый ключ из оснастки CA как .key не копируют.

Посмотреть, что корень самоподписан:

certutil -dump C:\ca.crt

В дампе Subject и Issuer совпадают, в расширениях есть CA.

Сертификат сайта

Заявка с SAN — файл site.inf. Exportable = TRUE нужен, только если потом заберёшь ключ в .pfx для nginx или другого сервиса не из хранилища Windows. Для IIS ключ может остаться в машине.

[NewRequest]
Subject = "CN=site.example.com"
KeyAlgorithm = ECDSA_P256
HashAlgorithm = SHA256
Exportable = TRUE
MachineKeySet = TRUE
RequestType = PKCS10

[Extensions]
2.5.29.17 = "{text}"
_continue_ = "dns=site.example.com&"

Имя CA в -config — то, что показывает certutil -config - -ping. Часто это hostname\Example Lab Root CA.

certreq -new C:\site.inf C:\site.csr
certreq -submit -config "dc.example.com\Example Lab Root CA" C:\site.csr C:\site.cer
certreq -accept C:\site.cer

-new делает пару и CSR (аналог site.key + site.csr, ключ остаётся в хранилище компьютера). -submit — подпись корнем, на выходе открытый site.cer. -accept связывает выданный сертификат с закрытым ключом в хранилище. Без accept сертификат есть, а IIS его не видит как пару.

Шаблон Web Server в оснастке CA должен разрешать выпуск, и у учётки, от которой идёт certreq, есть право Enroll. Если submit отвечает отказом по шаблону — это права и шаблон, не ошибка ключа.

Проверка:

certutil -verify -urlfetch C:\site.cer
certutil -dump C:\site.cer

В дампе ищешь dns=site.example.com. Нет SAN — браузер имя не примет, даже при верной подписи корня.

.cer и .pfx

.cer / .crt с CA — только открытая половина, её можно раздавать.

.pfx — контейнер с сертификатом и закрытым ключом. Экспорт: certlm.msc → Personal → Certificates → сертификат site.example.com → All Tasks → Export → Yes, export the private key. Это уже секрет, как site.key.

На Linux из pfx достают знакомые файлы:

openssl pkcs12 -in site.pfx -nokeys -out site.crt
openssl pkcs12 -in site.pfx -nocerts -nodes -out site.key
chmod 600 site.key

Итог

.crt — открытый сертификат, .key — закрытая половина пары. Корень сам себе подпись и источник доверия. Промежуточный подписывает листы и уезжает клиенту вместе с сертификатом сайта. fullchain.pem — это эти два открытых блока в одном файле, без ключей и без корня.

OpenSSL и AD CS делают одно и то же. Меняется только место, где лежит закрытый ключ корня: файл ca.key или хранилище Certification Authority.

mTLS добавляет к этой схеме вторую пару, уже у клиента, и второй контур доверия: сервер пускает только тех, чей сертификат выписал его клиентский CA.