Собственная PKI: выпуск сертификатов через OpenSSL
Для разработки, тестирования и экспериментов с mTLS в Netgap удобно развернуть собственную (private) PKI: свой корневой CA позволяет за минуты выпустить полный комплект сертификатов для локального стенда, не обращаясь к внешнему удостоверяющему центру. Эта статья — пошаговый практикум именно для такой тестовой инфраструктуры: создаем корневой CA, выпускаем серверный и клиентский сертификаты через OpenSSL и разбираем служебные детали, о которые чаще всего спотыкаются на практике.
Собственная PKI из этой статьи — инструмент для тестов и экспериментов, а не требование продукта. В производственной среде заказчик вправе использовать сертификаты любого удостоверяющего центра — публичного CA или собственного корпоративного УЦ, если таковой имеется.
Предполагается, что вы уже понимаете, как устроена цепочка доверия и какой алгоритм ключа выбрать. Проверять каждый полученный артефакт удобно командами из статьи Инспекция и проверка.
1. Шаг 1: создание корневого CA
Корневой CA — фундамент доверия: он создается один раз и надолго, поэтому ключ — максимально стойкий (см. рекомендации по алгоритмам).
# Приватный ключ CA — самый главный секрет всей системы
openssl genrsa -out ca.key 4096
chmod 600 ca.key
# Самоподписанный корневой сертификат на 10 лет
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \
-out ca.crt \
-subj "/CN=cluster.local" \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign,cRLSign"
Что означает каждый флаг:
| Флаг | Зачем |
|---|---|
-x509 | Не создавать запрос (CSR), а сразу выпустить готовый самоподписанный сертификат |
-nodes | Не шифровать ключ паролем (для автоматизации; для офлайн-корня пароль можно и оставить) |
-days 3650 | Срок действия 10 лет — стандарт для корневых CA, они должны жить долго |
-subj "/CN=cluster.local" | CN корня — имя домена доверия, чтобы администраторам было понятно, к какому кластеру относится корень |
-addext "basicConstraints=critical,CA:TRUE" | Самое важное: объявляет сертификат полноценным удостоверяющим центром, имеющим право подписывать другие сертификаты |
-addext "keyUsage=critical,keyCertSign,cRLSign" | Ключ разрешено использовать только для подписи сертификатов (keyCertSign) и списков отзыва (cRLSign) — использовать его для обычного TLS-трафика нельзя, что сужает поверхность атаки |
Без basicConstraints=critical,CA:TRUE современные библиотеки, включая rustls, откажутся принимать такой сертификат как корень доверия: для них это «обычный паспорт», которым нельзя проверять другие сертификаты. Это самая частая причина неработающего самодельного CA.
Два важных правила для корня:
- в корне не должно быть SPIFFE ID — корневой сертификат подтверждает лишь право выпускать другие сертификаты, а SPIFFE ID обозначает конкретную рабочую идентичность и живет в SAN конечных сертификатов (подробнее — в статье Идентичность клиента);
- поля subject и issuer у корня полностью совпадают — это главный признак самоподписанного сертификата. Проверить:
openssl x509 -in ca.crt -text -nooutиopenssl x509 -in ca.crt -purpose -noout.
2. Шаг 2: сертификат сервера
# Приватный ключ сервера
openssl genrsa -out server.key 2048
chmod 600 server.key
Создаем файл расширений server.ext — для современных систем (и rustls в частности) он обязателен:
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
DNS.2 = gw-1.cluster.local
IP.1 = 127.0.0.1
authorityKeyIdentifier— связывает сертификат с ключом издателя, помогая построить цепочку;basicConstraints=CA:FALSE— leaf-сертификат не имеет права подписывать другие сертификаты;keyUsageиextendedKeyUsage=serverAuth— сертификат пригоден именно для роли TLS-сервера;subjectAltName— обязателен: современные клиенты (rustls, браузеры, curl) сверяют имя сервера только с SAN и игнорируют CN. Сертификат без SAN не пройдет проверку имени, даже если CN совпадает с доменом.
Создаем CSR и подписываем его нашим CA:
# Запрос на подпись (CSR)
openssl req -new -key server.key -out server.csr -subj "/CN=gw-1.cluster.local"
# Подписание сертификата корневым CA (срок — 1 год)
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-out server.crt -days 365 -sha256 -extfile server.ext
Расширения из CSR при подписании через openssl x509 -req не переносятся — именно поэтому нужен -extfile server.ext. Проверка результата: openssl verify -CAfile ca.crt server.crt должна вывести server.crt: OK (что смотреть внутри CSR и сертификата — в статье Инспекция и проверка).
Современная альтернатива для leaf-ключа — Ed25519:
openssl genpkey -algorithm ed25519 -out mc_1_srv.key
openssl req -new -key mc_1_srv.key -out mc_1_srv.csr -subj "/CN=mc-1-srv"
openssl x509 -req -in mc_1_srv.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-out mc_1_srv.crt -days 365 -sha256 -extfile mc_1_srv.ext
openssl verify -CAfile ca.crt mc_1_srv.crt
# mc_1_srv.crt: OK
Все остальные шаги идентичны; в mc_1_srv.ext для сервиса Netgap в SAN вместо DNS-имен может стоять SPIFFE ID вида URI:spiffe://cluster.local/ns/petapp/sa/mc-1-srv — см. Идентичность клиента.
3. Шаг 3: сертификат клиента
Клиентский сертификат выпускается по той же схеме; отличаются назначение ключа и смысл CN:
openssl genrsa -out client.key 2048
chmod 600 client.key
Файл расширений client.ext — вместо serverAuth указываем clientAuth, SAN с DNS-именами не нужен:
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth
# В CN — имя сервиса, пользователя или устройства
openssl req -new -key client.key -out client.csr -subj "/CN=client-john-doe"
# -CAcreateserial уже не нужен: счетчик ca.srl создан на шаге 2
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key \
-out client.crt -days 365 -sha256 -extfile client.ext
Именно CN (или SPIFFE ID в SAN) клиентского сертификата сервер Netgap использует, чтобы понять, кто пришел, после того как rustls проверил цепочку — см. Идентичность клиента и mTLS на rustls.
4. Файл ca.srl: счетчик серийных номеров
После первого подписания рядом с ca.crt появляется файл ca.srl — его создал флаг -CAcreateserial. Внутри в шестнадцатеричном виде хранится серийный номер, который получит следующий подписанный сертификат.
Как это работает:
- при первом подписании OpenSSL видит
-CAcreateserial, генерирует случайное стартовое число, записывает его в сертификат и создаетca.srl; - при подписании следующего сертификата флаг уже не указывается: OpenSSL сам находит
ca.srl, читает номер, увеличивает его на единицу, присваивает новому сертификату и перезаписывает файл.
Правила обращения:
- удалять нельзя — без файла и без флага
-CAcreateserialследующее подписание завершится ошибкой: OpenSSL не найдет счетчик; - повторный
-CAcreateserialне читает старый файл, а полностью перезаписывает его новым случайным стартовым числом. Риск совпадения серийных номеров математически ничтожен (генерируется длинное случайное число), но последовательная нумерация выпущенных сертификатов теряется; - хранить — в одном каталоге с
ca.keyиca.crt, на офлайн-машине CA: это часть состояния удостоверяющего центра, как и сам ключ.
5. Шаг 4 (опционально): упаковка в PKCS#12
Если клиентский сертификат нужен браузеру, ОС или мобильному приложению, пара «отдельный .key + .crt» неудобна — их объединяют в защищенный паролем контейнер PKCS#12:
openssl pkcs12 -export -out client.p12 -inkey client.key -in client.crt -certfile ca.crt
# Команда запросит пароль; client.p12 устанавливается в систему двойным кликом (Windows/macOS)
Для компонентов Netgap на rustls этот шаг не нужен — они читают PEM-файлы напрямую.
6. Какие файлы у кого лежат
| Файл | CA (офлайн) | Сервер | Клиент | Комментарий |
|---|---|---|---|---|
ca.key | да | — | — | Главный секрет; никогда не покидает машину CA |
ca.srl | да | — | — | Состояние счетчика серийных номеров |
ca.crt | да | да | да | Публичный корень — хранилище доверия каждой стороны |
server.key | — | да | — | Секрет сервера, права 600 |
server.crt | — | да | — | Предъявляется клиенту при handshake |
client.key | — | — | да | Секрет клиента, права 600 |
client.crt | — | — | да | Предъявляется серверу при handshake |
*.csr, *.ext | да | — | — | Артефакты выпуска; можно хранить у CA для повторного выпуска |
Ручной выпуск через OpenSSL хорош для понимания и тестовых стендов. В боевых окружениях выпуск и ротацию сертификатов автоматизируют — Vault, cert-manager, service mesh: см. Автоматизация выпуска.
Когда все три уровня выпущены и openssl verify для каждого leaf выводит OK, PKI готова к использованию. Дальше — настройка mTLS на rustls и план ротации и автоматизации выпуска.