Несмотря на то, что в наше время существует 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.
ссылка на оригинал статьи https://habr.com/ru/post/686230/
Добавить комментарий