Для тестовых стендов, внутренних интеграций и mTLS между своими сервисами постоянно нужны сертификаты, которые никто внешний не выпустит: со своими SAN, сроками и расширениями. Правильный ответ — маленький локальный CA на openssl ca. Этот текст — переработка моей рабочей шпаргалки, которой пользуюсь много лет: полный цикл от конфига до отзыва, с расшифровкой каждого флага.

Команды актуальны для OpenSSL 3.x; там, где поведение старых версий (1.0.2/1.1.1) отличается, стоит пометка ⚠ legacy. Всё — Linux; никакой Windows-специфики.

Структура каталогов

openssl ca — это простейшая «база данных» удостоверяющего центра на плоских файлах. Рабочий каталог:

/opt/ca/
├── openssl.cnf        # конфиг (ниже)
├── CA/                # сертификат и ключ самого CA
│   ├── CAcert.pem
│   └── CAkey.pem
├── DB/db              # реестр выпущенных сертификатов (изначально пустой)
├── DB/db.attr         # unique_subject = yes
├── serial/serial      # счётчик серийников (изначально: 00)
├── newcerts/          # копии всех выпущенных сертификатов (кладёт сам openssl)
├── certs/             # рабочая папка для текущих операций
└── CRL/               # списки отзыва
1
2
3
4
mkdir -p /opt/ca/{CA,DB,serial,newcerts,certs,CRL}
touch /opt/ca/DB/db
echo 'unique_subject = yes' > /opt/ca/DB/db.attr
echo 00 > /opt/ca/serial/serial

Пара слов о неочевидном:

  • DB/db — реестр: по строке на каждый выпущенный сертификат (статус, серийник, subject). Именно по нему строится CRL. unique_subject = yes запрещает выпустить два действующих сертификата с одинаковым subject — для перевыпуска сначала отзывайте старый (или ставьте no, если такой контроль не нужен);
  • serial — следующий серийный номер. Со значением 00 номера идут по порядку. Альтернатива — случайные 128-битные серийники: опция -create_serial при подписании, файл serial тогда не нужен. ⚠ legacy: в 1.0.2 пустой serial без -create_serial роняет команду с ошибкой.

Конфиг openssl.cnf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
[ ca ]
default_ca = CA_default

[ CA_default ]
dir             = /opt/ca
certificate     = $dir/CA/CAcert.pem
private_key     = $dir/CA/CAkey.pem
crl             = $dir/CRL/crl.pem
database        = $dir/DB/db
new_certs_dir   = $dir/newcerts
serial          = $dir/serial/serial
default_days    = 730
default_crl_days= 30
default_md      = sha256
policy          = policy_match
x509_extensions = usr_ext
crl_extensions  = crl_ext
copy_extensions = copy      # переносить SAN из CSR в сертификат (см. ниже)

[ policy_match ]
commonName              = supplied
organizationName        = supplied
organizationalUnitName  = optional
stateOrProvinceName     = optional
localityName            = optional
countryName             = supplied
emailAddress            = optional

[ req ]
default_bits        = 2048
default_md          = sha256
distinguished_name  = req_distinguished_name
x509_extensions     = ca_ext      # используется при -x509 (самоподписанный CA)

[ req_distinguished_name ]
countryName             = Country (C)
countryName_default     = RU
localityName            = Locality (L)
localityName_default    = Moscow
organizationName        = Organization (O)
organizationName_default = Example Corp
organizationalUnitName  = Organizational Unit (OU)
commonName              = Common Name (CN)

[ ca_ext ]                        # расширения корневого сертификата
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid:always,issuer:always
basicConstraints        = critical, CA:true
keyUsage                = critical, keyCertSign, cRLSign

[ usr_ext ]                       # расширения выпускаемых сертификатов по умолчанию
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid:always,issuer:always
basicConstraints        = CA:false

[ crl_ext ]
authorityKeyIdentifier  = keyid:always,issuer:always

[ server_ext ]                    # профиль серверного сертификата
keyUsage         = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName   = DNS:svc.example.org, DNS:svc-test.example.org

[ client_ext ]                    # профиль клиентского (mTLS)
keyUsage         = critical, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth

Ключевые отличия от типовых конфигов десятилетней давности:

  • SAN обязателен. Современные клиенты (браузеры, Go, Java 11+) игнорируют CN и проверяют имя только по subjectAltName — серверный сертификат без SAN бесполезен. SAN задаётся в профиле (server_ext) или переносится из CSR (copy_extensions = copy; включайте осознанно — CA начинает доверять расширениям из запроса);
  • Профили-секции (server_ext / client_ext) выбираются при подписании ключом -extensions — один CA выпускает и серверные, и клиентские сертификаты;
  • Пароль CA в конфиге не храним — никакая секция с passphrase в файле не нужна, openssl спросит его интерактивно (или берите из окружения при автоматизации).

Создание CA

1
2
3
cd /opt/ca
openssl req -config openssl.cnf -x509 -new -days 3652 \
  -out CA/CAcert.pem -keyout CA/CAkey.pem
ФлагЧто делает
req -x509вместо запроса на подпись сразу выпустить самоподписанный сертификат
-newсоздать новый запрос (и новый ключ, раз -key не указан)
-days 3652срок действия — 10 лет (для CA нормально)
-out / -keyoutфайлы сертификата и приватного ключа

Openssl спросит passphrase для ключа CA (запишите в менеджер паролей — восстановить нельзя) и поля DN. Ненужное поле пропускается вводом . (точки).

⚠ legacy: в 1.0.2 вывод начинается со строки Loading 'screen' into random state — это норма тех версий, в 3.x её нет.

Проверить, что получилось:

1
openssl x509 -in CA/CAcert.pem -noout -text

Ключ и запрос на сертификат (CSR)

Ключ:

1
openssl genrsa -out certs/svc.key 2048
ВариантКогда
genrsa ... 2048стандартный RSA-2048, максимум совместимости
genrsa -aes256 ...зашифрованный ключ: спросит passphrase, и её же будет спрашивать при каждом использовании ключа
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out svc.keyECDSA: ключи и подписи короче, поддержка давно повсеместная

⚠ legacy: genrsa в 3.x пишет ключ в формате PKCS#8 (-----BEGIN PRIVATE KEY-----), а 1.0.2 — в PKCS#1 (-----BEGIN RSA PRIVATE KEY-----). Функционально для TLS это одно и то же, но парсеры некоторых старых приложений понимают только PKCS#1: конвертация — openssl rsa -traditional (3.x).

Запрос:

1
openssl req -config openssl.cnf -new -key certs/svc.key -out certs/svc.csr

Если SAN приходит из запроса (а не из профиля CA), добавьте его прямо из командной строки — без правки конфига:

1
2
openssl req -config openssl.cnf -new -key certs/svc.key -out certs/svc.csr \
  -addext 'subjectAltName=DNS:svc.example.org,DNS:svc-test.example.org'

⚠ legacy: -addext появился в 1.1.1; в 1.0.2 SAN в CSR добавляется только через секцию конфига (-reqexts).

Проверка запроса перед отправкой/подписанием:

1
openssl req -in certs/svc.csr -noout -text

Подписание

1
2
openssl ca -config openssl.cnf -in certs/svc.csr -out certs/svc.pem \
  -days 730 -extensions server_ext
ФлагЧто делает
-days 730срок; без флага возьмётся default_days из конфига
-extensions server_extпрофиль расширений (серверный/клиентский)
-startdate 20260801000000Z(опционально) сертификат действует с указанного момента UTC — удобно для заранее подготовленной ротации
-create_serial(опционально) случайный 128-битный серийник

CA покажет содержимое будущего сертификата и попросит подтвердить дважды: подписать и записать в реестр. Копия ляжет в newcerts/<serial>.pem, строка — в DB/db.

Файл на выходе содержит и текстовую расшифровку, и PEM-блок. Если принимающая система требует «чистый» PEM — вырезаем только блок:

1
openssl x509 -in certs/svc.pem -out certs/svc.crt   # оставит только сертификат

Проверка соответствия ключ ↔ CSR ↔ сертификат

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

1
2
3
openssl pkey -in svc.key -pubout | openssl sha256
openssl req  -in svc.csr -pubkey -noout | openssl sha256
openssl x509 -in svc.crt -pubkey -noout | openssl sha256

Проверка цепочки до CA:

1
openssl verify -CAfile CA/CAcert.pem certs/svc.crt

И сроки действия одной строкой:

1
openssl x509 -in certs/svc.crt -noout -dates

Отзыв и CRL

Отзыв — это отметка в реестре плюс перевыпуск CRL:

1
2
3
4
5
# пометить сертификат отозванным (берём копию из newcerts/ по серийнику)
openssl ca -config openssl.cnf -revoke newcerts/01.pem

# перевыпустить список отзыва
openssl ca -config openssl.cnf -gencrl -out CRL/crl.pem

Посмотреть, кто отозван:

1
openssl crl -in CRL/crl.pem -noout -text

CRL имеет собственный срок (default_crl_days) — по его истечении клиенты, проверяющие отзыв, перестанут принимать список, так что перевыпуск CRL нужно поставить в cron, даже если никто не отзывался.

PKCS#12: контейнер «ключ + сертификат + цепочка»

Один файл с ключом, сертификатом и CA — стандартный способ передать identity в Java-приложения, nginx-плагины, браузеры:

1
2
openssl pkcs12 -export -in certs/svc.crt -inkey certs/svc.key \
  -certfile CA/CAcert.pem -name svc -out certs/svc.p12
ФлагЧто делает
-exportрежим создания контейнера (без него — чтение)
-in / -inkeyсертификат и его приватный ключ
-certfileдобавить цепочку (CA / промежуточные)
-namealias записи внутри контейнера (Java его увидит)

⚠ legacy: OpenSSL 3.x шифрует PKCS#12 современными алгоритмами (AES/PBES2), и старые потребители (Java 8 без обновлений, openssl 1.0.2) могут его не открыть. Для совместимости со старьём добавьте -legacy. И наоборот: старый p12 в 3.x открывается командой openssl pkcs12 -in old.p12 -legacy ....

Достать всё обратно:

1
openssl pkcs12 -in certs/svc.p12 -nodes -out bundle.pem   # ключ+серты в PEM

Конвертации форматов

Сертификат один и тот же — упаковки разные. Быстрый определитель: PEM — текст с -----BEGIN ...-----; DER — бинарный; PKCS#7 (.p7b) — контейнер только для сертификатов (без ключей); PKCS#12 (.p12/.pfx) — контейнер с ключами.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# DER → PEM (сертификат)
openssl x509 -in cert.der -inform DER -out cert.pem

# PEM → DER
openssl x509 -in cert.pem -outform DER -out cert.der

# PKCS#7 → PEM (вытащить все сертификаты)
openssl pkcs7 -print_certs -in bundle.p7b -inform DER -out certs.pem

# ключ DER → PEM
openssl pkey -in key.der -inform DER -out key.pem

Шпаргалка «посмотреть глазами»

1
2
3
4
5
6
7
8
openssl x509 -in cert.pem -noout -text          # сертификат целиком
openssl x509 -in cert.pem -noout -dates         # только сроки
openssl x509 -in cert.pem -noout -ext subjectAltName   # только SAN
openssl req  -in req.csr  -noout -text          # запрос
openssl crl  -in crl.pem  -noout -text          # список отзыва
openssl pkcs12 -in store.p12 -info -nokeys      # содержимое p12
openssl s_client -connect host:443 -servername host </dev/null 2>/dev/null \
  | openssl x509 -noout -dates                  # что реально отдаёт сервер

Продолжение — про хранилища Java (keytool, PKCS#12 vs JKS, импорт цепочек и типовые операции) — в отдельном посте.