Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Несмотря на то, что в наше время существует letsencrypt и подобные сервисы с утилитами, которые всё делают сами, я считаю что каждый айтишник должен уметь выпускать самоподписанные сертификаты и иметь минимальное базовое понимание их работы.
К тому же, в собственной сети не так важен правильный сертификат, когда на всех клиентских устройствах можно предустановить свой доверенный корневой сертификат и подписывать им сертификаты внутренних ресурсов. Но у автора статьи просто был спортивный интерес и на досуге хотелось вникнуть в тему.
Я изучаю на досуге Java и мне потребовалось дома в локальной сети настроить тестовый полигон для mTLS аутентификации по сертификатам, это когда клиент аутентифицирует сервер, а сервер клиента.
Общение клиента с сервером mTLS
На данной схеме не рассмотрены все тонкости взаимных рукопожатий, но базовые моменты выглядят примерно так.
При рукопожатиях сервер сообщает что поддерживает клиентскую аутентификацию по сертификату, это один из важных моментов для многих клиентов, особенно для браузеров, если сервер неправильно поздоровается, то клиент не станет ему отправлять свой сертификат, но тот же curl и insomnia умеют отправлять сертификат даже когда не просят.
В браузере, когда у вас есть клиентские сертификаты и сервер их запрашивает это выглядит так:
Цепочки подписания сертификатов
Для наших опытов будет применяться следующая цепочка подписания сертификатов.
HomeLabCA - это самый корневой сертификат, но конечные сертификаты (клиентские и серверные) им не подписываются, во-первых в данном эксперименте хотелось поиграться с цепочками сертификатов, во-вторых это достаточно распространенная практика - корневой центр сертификации обычно подписывает сертификаты других удостоверяющих центров, а они уже подписывают конечные сертификаты.
Для подписания сертификатов требуется секретный ключ вышестоящего и при массовом выпуске конечных сертификатов лишний раз не светится и не распространяется ключ корневого сертификата, вместо этого используются промежуточные центры сертификации и при вероятной компрометации одного из промежуточных не пострадают сертификаты выпущенные выпущенные другими промежуточными центрами сертификации.
Допустим, что случилось непоправимое, один из промежуточных сертификатов HomeLabCA2 был украден, злоумышленниками был похищен приватный ключ и началось бесконтрольное подписание сертификатов для фишинговых сайтов, в таком случае корневой центр может отозвать этот промежуточный сертификат и все подписанные им сертификаты сразу потеряют доверие, но но сертификат srv1.loc при этом продолжит действовать, а если бы все сертификаты подписывались только корневым HomeLabCA, то масштаб компрометации был бы намного больше.
Для наглядного примера можно посмотреть в браузере путь сертификации у сайта habr.com
Генерация сертификатов
Предупреждение: Если такое будете повторять на проде - нужно внимательно подойти к тонким настройкам, моей целью было просто настроить несколько виртуальных машин на домашнем компе, сделать взаимодействие между сервисами с mTLS, в общем, всё только в учебных целях, организация тестового полигона для опытов, в проде, вероятно, лучше задействовать LetsEncrypt. К тому же нужно
Вернемся к первой картинке, будем выполнять всё по схеме
Структура каталогов нашего игрушечного центра сертификации:
├──./certs
│ ├── /ca_root - корневой, главный УЦ
│ │ ├── openssl.conf - конфиг корневого УЦ
│ │ ├── index.txt - база данных сертификатов
│ │ ├── serial - счетчик серийных номеров
│ │ ├── /pub - каталог с открытыми ключами
│ │ ├── /priv - каталог с закрытыми ключами
│ │ ├── /newcerts - каталог с подписанными сертификатами
│ │
│ ├── /ca1 - промежуточный УЦ
│ │ ├── openssl.conf - конфиг промежуточного УЦ
│ │ ├── index.txt - база данных сертификатов
│ │ ├── serial - счетчик серийных номеров
│ │ ├── /pub - каталог с открытыми ключами
│ │ ├── /priv - каталог с закрытыми ключами
│ │ ├── /newcerts - каталог с подписанными сертификатами
│ │ ├── /csr - каталог с запросами на подпись
│ │ ├── /psk12 - каталог с ключевыми парами
Для всех манипуляций нам потребуется только одна утилита openssl, можно воспользоваться git bash под Windows, либо WSL, либо на виртуалке с линуксом
Я буду использовать в WSL с Debian, поэтому, корневая папка будет /mnt/d/cert
mkdir /mnt/d/cert
Далее зайдем в созданную папку и создам всю структуру каталогов и файлов из схемы выше
cd /mnt/d/cert
mkdir -p {ca_root,ca1}/{pub,priv,newcerts}
echo 1000 > ca_root/serial
echo 1000 > ca1/serial
touch {ca_root,ca1}/{index.txt,openssl.conf}
mkdir ca1/{csr,psk12}
Корневой центр сертификации
Откроем файл ca_root/openssl.conf и впишем в него следующее
ca_root/openssl.conf
[ ca ]
default_ca = CA_default
[ CA_default ]
dir = /mnt/d/cert/ca_root #папка с нашим УЦ
certs = $dir/priv
#crl_dir = $dir/crl
new_certs_dir = $dir/newcerts
database = $dir/index.txt
serial = $dir/serial
RANDFILE = $dir/priv/.rand
#подписывающие серты
private_key = $dir/priv/ca.key
certificate = $dir/pub/ca.crt
default_md = sha256
name_opt = ca_default
cert_opt = ca_default
default_days = 375
preserve = no
policy = policy_strict
[ policy_strict ]
countryName = match
stateOrProvinceName = match
organizationName = optional
organizationalUnitName = optional
commonName = optional
emailAddress = optional
[ req ]
default_bits = 2048
distinguished_name = req_distinguished_name
string_mask = utf8only
default_md = sha256
x509_extensions = v3_ca
[ req_distinguished_name ]
countryName = Country Name (2 letter code)
stateOrProvinceName = State or Province Name
localityName = Locality Name
organizationName = Organization Name
organizationalUnitName = Organizational Unit Name
commonName = Common Name
emailAddress = Email Address
# дефолтные значения
countryName_default = RU
stateOrProvinceName_default = Russia
localityName_default = Russia
organizationName_default = MyHomeLab
[ v3_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
[ v3_intermediate_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
Дальше перейдем в папку с корневым ЦС и сгенерируем приватный ключ, у нас спросит пароль, не забывайте его, он будет требоваться для дальнейших манипуляций
cd ./ca_root
openssl genrsa -aes256 -out priv/ca.key 4096
Создадим самоподписанный корневой сертификат
openssl req -config openssl.conf \
-key priv/ca.key \
-new -x509 -days 7300 -sha256 -extensions v3_ca \
-out pub/ca.crt
Основываясь на нашей договоренности из первой схемы у корневого сертификата будет CommonName=HomeLabCA
Корневой сертификат готов, теперь переходим к промежуточному ЦС - HomeLabCA1
Промежуточный ЦС
Подготовим конфигурационный файл в ca1 - ca1/openssl.conf
ca1/openssl.conf
[ ca ]
default_ca = CA_default
[ CA_default ]
dir = /mnt/d/cert/ca1 # папка промежуточного цс
certs = $dir/pub
new_certs_dir = $dir/newcerts
database = $dir/index.txt
serial = $dir/serial
RANDFILE = $dir/priv/.rand
private_key = $dir/priv/ca1.key
certificate = $dir/pub/ca1.crt
default_md = sha256
name_opt = ca_default
cert_opt = ca_default
default_days = 375
preserve = no
policy = policy_loose
unique_subject = no
[ policy_loose ]
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ req ]
default_bits = 2048
distinguished_name = req_distinguished_name
string_mask = utf8only
default_md = sha256
x509_extensions = v3_ca
[ req_distinguished_name ]
countryName = Country Name (2 letter code)
stateOrProvinceName = State or Province Name
localityName = Locality Name
organizationName = Organization Name
organizationalUnitName = Organizational Unit Name
commonName = Common Name
emailAddress = Email Address
# значения по-умолчанию
countryName_default = RU
stateOrProvinceName_default = Russia
localityName_default = Russia
organizationName_default = MyHomeLab1
[ v3_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
[ v3_intermediate_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
[ usr_cert ]
basicConstraints = CA:FALSE
nsCertType = client, email
nsComment = "OpenSSL Generated Client Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, emailProtection
[ server_cert ]
basicConstraints = CA:FALSE
nsCertType = server
nsComment = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName=${ENV::SAN}
И снова генерируем приватный ключ, только уже для промежуточного ЦС, запоминаем пароль для ключа
cd ../ca1
openssl genrsa -aes256 -out priv/ca1.key 4096
Дальше мы уже будем создавать подписанный сертификат, поэтому, создаем запрос на подписание сертификата, тут нам понадобится пароль от ключа priv/ca1.key
openssl req -config openssl.conf -new -sha256 \
-key priv/ca1.key \
-out csr/ca1.csr
Не забываем указать cname=HomeLabCA1 по изначальной схеме на картинке выше.
Далее подписываем промежуточный сертификат корневым
openssl ca -config ../ca_root/openssl.conf -extensions v3_intermediate_ca \
-days 3650 -notext -md sha256 \
-in csr/ca1.csr \
-out pub/ca1.crt
На этом этапе мы подписали корневым сертификатом наш промежуточный, дальше все манипуляции с выпуском сертификатов будут проводиться уже на промежуточном, теперь можно положить приватный ключ корневого сертификата на пару внешних дисков и убрать далеко в сейф, он нам не потребуется, если не будем создавать еще один промежуточный центр сертификации.
Генерация серверного сертификата
Итак, мы находимся в папке промежуточного центра сертификации, нам нужно выпустить сертификат для сервера с адресом srv1.loc и ip-адресом 192.168.200.10.
Основной момент при генерации сертификатов для сервера - это вписать SAN, это альтернативные имена для нашего сертификата, на практике выяснилось, что браузеры не особо смотрят на cname, а ориентируются на SAN. Если вы внимательно почитали конфиг то увидели в разделе [ server_cert ] следующую строку:
subjectAltName=${ENV::SAN}
Эта строчка подставляет из переменной окружения SAN значение в атрибут subjectAltName.
Первым делом, заходим в папку ca1 нашего промежуточного центра сертификации и генерируем приватный ключ, только тут упущена опция -aes256, поэтому ключ будет без пароля, это всё чтобы не вводить пароль при перезагрузке nginx
cd ../ca1
openssl genrsa -out priv/192.168.200.10.key 2048
Дальше всё по аналогии, создаем запрос на подпись, указываем cname=srv1.loc
openssl req -config openssl.conf \
-key priv/192.168.200.10.key \
-new -sha256 -out csr/192.168.200.10.csr
Теперь подписываем промежуточным сертификатом наш серверный srv1.loc, но предварительно заполним переменную окружения SAN
export SAN=DNS:site1.loc,IP:192.168.200.10
openssl ca -config openssl.conf \
-extensions server_cert -days 375 -notext -md sha256 \
-in csr/192.168.200.10.csr \
-out pub/192.168.200.10.crt
созданный сертификат в картинках
Цепочки сертификатов
Цепочка сертификатов включает себя все промежуточные центры сертификации вплоть до корневого.
Каждый сертификат начинается с -----BEGIN CERTIFICATE----- и заканчивается -----END CERTIFICATE-----
В данном случае полная цепочка для серверного сертификата будет выглядеть так:
-----BEGIN CERTIFICATE-----
сам серверный сертификат
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
сертификат промежуточного ЦС
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
сертификат корневого ЦС
-----END CERTIFICATE-----
Создадим сперва цепочку для промежуточного, добавив туда корневой (открытый) сертификат:
cat pub/ca1.crt ../ca_root/pub/ca.crt > pub/ca1_chain.crt
Далее создадим цепочку для серверного сертификата
cat pub/192.168.200.10.crt pub/ca1_chain.crt > pub/192.168.200.10_chain.crt
Цепочка серверного сертификата понадобится нам при настройке сервера, но это будет дальше, сейчас мы сделаем два клиентских сертификата...
Генерация клиентских сертификатов
Всё по аналогии, сперва генерируем приватный ключ, всё будем в параллели делать сразу для двух ключей
openssl genrsa -out priv/client1.key 2048
openssl genrsa -out priv/client2.key 2048
Теперь создаем запросы на подпись, не забываем указывать cname = client1 и client2 соответственно
openssl req -config openssl.conf \
-key priv/client1.key -new -sha256 \
-out csr/client1.csr
openssl req -config openssl.conf \
-key priv/client2.key -new -sha256 \
-out csr/client2.csr
Подписываем оба сертификата по очереди
openssl ca -config openssl.conf -extensions usr_cert \
-days 375 -notext -md sha256 \
-in csr/client1.csr \
-out pub/client1.crt
openssl ca -config openssl.conf -extensions usr_cert \
-days 375 -notext -md sha256 \
-in csr/client2.csr \
-out pub/client2.crt
Создание ключевой пары
В систему просто так не инсталлировать отдельно ключ и отдельно открытый сертификат, их нужно объединить в ключевую пару p12, создадим две ключевые пары для двух сертификатов - client1 и client2. Помните, выше мы создавали цепочку ca1_chain.crt нам она сейчас пригодится, это цепочка подписантов.
openssl pkcs12 -export \
-in pub/client1.crt \
-inkey priv/client1.key \
-certfile pub/ca1_chain.crt \
-out psk12/client1.p12 \
-passout pass:123321
Тоже самое повторим для второго клиентского ключа
openssl pkcs12 -export \
-in pub/client2.crt \
-inkey priv/client2.key \
-certfile pub/ca1_chain.crt \
-out psk12/client2.p12 \
-passout pass:123321
На выходе у нас будет два файла, которые можно установить в систему
На данном этапе у нас установлены ca.crt, ca1.crt, и два клиентских сертификата, но чтобы провести первые эксперименты, потребуется настроить тестовый сервер.
Настройка тестового сервера Nginx с TLS
Данный этап будет проходить уже на Linux-машине, я скопирую всю папку туда, соответствие файлов будет по аналогии, положу всю папку cert в /opt/ssl/
Помимо nginx я установил php-fpm, мне потребуется простейший скрипт для отладки.
Конфиг nginx
server {
listen 80 default_server;
listen [::]:80 default_server;
# SSL configuration
#
listen 443 ssl default_server;
ssl_certificate /opt/ssl/cert/ca1/pub/192.168.200.10_chain.crt;
ssl_certificate_key /opt/ssl/cert/ca1/priv/192.168.200.10.key;
#mtls
ssl_client_certificate /opt/ssl/cert/ca1/pub/ca1_chain.crt;
ssl_verify_client optional;
root /www;
index index.php;
server_name _;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php7.4-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param X-SSL-CERT $ssl_client_cert;
fastcgi_param X-SSL-VERIFIED $ssl_client_verify;
fastcgi_param X-SSL-CLIENT-DN $ssl_client_s_dn;
fastcgi_param X-SSL-ISSUER-DN $ssl_client_i_dn;
fastcgi_param HTTP_PROXY "";
fastcgi_param SSL_CLIENT_SERIAL $ssl_client_serial;
}
}
Тестовый скрипт /www/index.php
<pre>
<?php print_r($_SERVER); ?>
POST=
<?php print_r($_POST)?>
GET=
<?php print_r($_GET)?>
RAW=
<?=file_get_contents('php://input')?>
</pre>
Дальше проверяем конфиг и перезагружаем nginx
nginx -t
service nginx restart
Открываем приватное окно в хроме или яндекс браузере, для firefox отдельная история с собственными хранилищами.
При входе на сайт у нас идет запрос сертификата, как мы помним, сервер сообщает о поддержки аутентификации и поэтому браузер запрашивает.
С серверным сертификатом тоже всё отлично, браузеры не ругаются
В итоге у нас есть комплект клиентских сертификатов, и есть сервер, который возвращает отладочную информацию, можно тренироваться.
Чтобы curl не ругался на сертификат, ему нужно указать цепочку доверенных
curl -s \
--cacert cert/ca1/pub/ca1_chain.crt \
https://192.168.200.10
Далее можно указать пару ключей для авторизации по ключу и проверить с каким ключом мы пришли
curl -s \
--cacert cert/ca1/pub/ca1_chain.crt \
--key cert/ca1/priv/client1.key \
--cert cert/ca1/pub/client1.crt \
https://192.168.200.10 | grep X-SSL-CLIENT-DN
Но если указать сертификат, который подписан отличным от нашего ЦС, то приложению не будет передана информация о сертификате и X-SSL-VERIFIED будет NONE.
Можно указать не опциональную, а строгую проверку сертификатов в nginx, так, чтобы нельзя было без сертификата ничего прислать
ssl_verify_client on;
В финале у нас есть центр сертификации для генерации любого количества сертификатов, тестовый сервер, выводящий всю информацию, которая ему пришла и комплект всех ключей, можно отлаживать софт, использующий клиентские сертификаты либо потренироваться настраивать разные сервера и сервисы.
А еще утилита openssl умеет отлаживать настройку серверных сертификатов, но это уже тема для отдельной статьи
openssl s_client -connect 192.168.200.10:443
Спасибо за внимание, если тема будет интересной, то в следующей статье постараюсь углубиться в другие тонкости сертификатов и утилиты openssl.