Статья
Пара сертификата: .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:
- Браузер говорит имя (
site.example.com) и предлагает параметры. - Сервер отдаёт
fullchain.pem: сначалаsite.crt, сразу за нимintermediate.crt. Корень не шлёт. - Браузер проверяет цепочку до доверенного корня, срок, что имя из адресной строки есть в SAN.
- Сервер доказывает, что у него есть
site.keyот этого сертификата: подписывает кусок рукопожатия. Закрытый ключ по сети не уходит. - Обе стороны получают сессионные ключи через обмен (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 в домене:
- Server Manager → Add Roles and Features → Active Directory Certificate Services → роль Certification Authority.
- Тип: Enterprise CA (нужен домен; Standalone тоже умеет подписывать, но без шаблонов и автопубликации в AD).
- Root CA, новый ключ: RSA 2048 или ECDSA P-256, хеш SHA256.
- Имя, например
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.