January 16, 2025

Прикончить деда: переход с Basic Auth на mTLS

Интро

Как часто вы хотели что-то развернуть и думали, а как прикрыть это от внешнего мира? Я довольно часто.

Есть у меня своего рода фетиш, разворачивать разные сервисы для личных нужд. Например:

  • Gitea, для синхронизации Obsidian
  • Сhangedetection, для мониторинга изменений на сайтах
  • Hive от Hexway, для ведения файдингов на багбанти
  • n8n для автоматизаций
    и многое другое что можно найти в репозитории awesome-selfhosted

Если в части сервисов у меня нет важной информации, то с Hive ситуация совершенно иная и его необходимо прикрыть дополнительным слоем защиты.

Первым на ум приходил старик Basic Auth. Но он подкидывает проблем тк подвергается бруту да и передается через заголовки преобразованных в base64.
В один прекрасный момент я вспомнил, что еще в далеком 2014 настраивал доступ к сервису RoundCube (Web интерфейс для работы с почтой) через mTLS.

Кто же такой mTLS?
Простым языком, это проверка сертификата с двух сторон. Не только Вы проверяете, что сервер имеет нужный сертификат, но и сам сервер запрашивает ваш клиентский сертификат для проверки и предоставления доступа.

Зачем это все нужно

Могу отметить следующие плюсы:

  • Это удобно, вам не потребуется дополнительно вводить логин и пароль от Basic Auth
  • Если Вы по какой-то причине не умели в сертификаты, у вас появится https вместо http.
  • Вам перестанут брутфорсить Basic Auth
  • Логин и пароль проще украсть нежели сертификат, следовательно это безопаснее

Процесс настройки

Не пугайтесь, да этот процесс сложнее Basic Auth, но я опишу все под ctrl+c, ctrl+v.
Так же в этой статье не будет страшных слов, таких как Vault от Hashicorp, все постараюсь описать максимально просто.

Для ознакомления с механизмом, пример будет с использованием самоподписанного сертификата, где корневой центр сертификации и сервис доступный через mTLS размещены на одном сервере.

Серверная сторона

Представим, что у вас есть сервер доступный через интернет с Ubuntu Linux на борту.

Преднастройка

ПО

Если у вас уже установлен nginx и openssl - хорошо. Если нет, то:

sudo apt install openssl nginx
sudo systemctl enable nginx
sudo service nginx start
Конечно nginx у нас может быть в docker, но сегодня мы рассмотрим распространненную ситуацию, без глубокого погружения в сети и конфигурирования взаимодействия между docker контейнерами.

Развернем тестовое приложение docker:

sudo docker run -d --rm -p 127.0.0.1:3000:3000 bkimminich/juice-shop
Если docker не установлен, то вам сюда https://docs.docker.com/engine/install/ubuntu/
После выполнения команды будет запущен Juice Shop на http://127.0.0.1:3000

Центр Сертификатов

Создание каталогов для работы с сертификатами:

sudo mkdir -p /etc/ssl/mtls/ca/{certs,crl,newcerts,private}
sudo chmod 700 /etc/ssl/mtls/ca/private
sudo touch /etc/ssl/mtls/ca/index.txt

Создадим индекс файл для хранения информации о сертификатах:

sudo touch /etc/ssl/mtls/ca/index.txt

Создадим файлы с id от которых пойдет отсчет наших выданных и отозванных сертификатов (исторически id от 1000):

sudo echo 1000 > /etc/ssl/mtls/ca/crlnumber
sudo echo 1000 > /etc/ssl/mtls/ca/serial

Создадим свой файл конфигурации openssl:

sudo vim /etc/ssl/mtls/openssl.cnf
(!) Обратите особое внимание на блок [ alt_names ], в примере ниже использован мой домен, его 100% необходимо заменить на ваши данные
[ ca ]
default_ca = CA_default

# Указываем данные о местонахождении файлов и параметры для выпуска CA
# в данном примере CA выпускается на 10 лет
[ CA_default ]
dir               = /etc/ssl/mtls/ca
certs             = $dir/certs
crl_dir           = $dir/crl
database          = $dir/index.txt
new_certs_dir     = $dir/newcerts
certificate       = $dir/cacert.pem
serial            = $dir/serial
crlnumber         = $dir/crlnumber
crl               = $dir/crl.pem
private_key       = $dir/private/cakey.pem
default_md        = sha256
default_days      = 3650
policy            = policy_match
default_crl_days  = 30

# Настройки политик проверки полей в выдаваемых сертификатах
# должны совпадать по countryName, stateOrProvinceName, organizationName
[ policy_match ]
countryName       = match
stateOrProvinceName = match
organizationName  = match
commonName        = supplied

# Настройки для запросов создания сертификатов
[ req ]
default_bits      = 4096
default_md        = sha256
distinguished_name = req_distinguished_name
x509_extensions   = v3_ca

# Описание формата DN (distinguished name) при создании сертификатов
[ req_distinguished_name ]
countryName         = Country Name (2 letter code)
countryName_default = RU
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = Saint Petersburg
localityName        = Locality Name (eg, city)
localityName_default = Saint Petersburg
organizationName    = Organization Name (eg, company)
organizationName_default = Evil Corp
commonName          = Common Name (eg, YOUR name)
commonName_default  = Put new data here (CA or dns or user)

# Дополнительная конфигурация для CA сертификатов
# basicConstraints = CA:TRUE - параметр, который позволяет сертификату 
# подписывать иные сертификаты
# keyUsage - может использоваться для подписания сертификатов и CRL (Отзыв сертификатов)
# subjectKeyIdentifier - идентификатор будет хэшом
# authorityKeyIdentifier:
# keyid:always - всегда включать идентификатор CA ключа в сертификат
# issuer - Добавление в сертификат информации о DN CA сертификата
[ v3_ca ]
basicConstraints = CA:TRUE
keyUsage = keyCertSign, cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer

# Дополнительные опции для реквеста сертификата server
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
authorityKeyIdentifier = keyid,issuer
subjectAltName = @alt_names

# Важное поле в котором перечисляются домены и IP в поле Alternative Name
# У меня wildcard сертификат, но можно перечислить имена и по одному
# Если планируется сертификат для взаимодействия по IP, то
# указать это нужно в формате IP.1 = ваш_ip следующей строкой можно 
# добавить еще адрес написав IP.2= ваш_второй_ip
[ alt_names ]
DNS.1 = *.0q.lol

# Дополнительные опции реквеста для сертификатов клиента
# Обратите внимание на параметр extendedKeyUsage, здесь он clientAuth
# это означает, что сертификаты выпущенные с этим блоком настроек 
# будут передаваться для аутентификации клиента
[ v3_client ]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth
authorityKeyIdentifier = keyid,issuer

Создание сертификатов

CA cert

Создание ключа корневого сертификата:
Во многих статьях показан процесс сначала генерации ключа, затем выпуска сертификата. Но я человек ленивый и делаю все в одном

(!) Потребуется ввести пароль, это самый главный пароль который стоит хорошо спрятать и не потерять он на потребуеться довольно часто.
(!) Будет интерактивное меню ввода данных, часть из них можно предзаполнить в нашем openssl.cnf
(!) В данном случае commonName можно заполнить как угодно, например Evil Corp CA
sudo openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 \
  -keyout /etc/ssl/mtls/ca/private/cakey.pem \
  -out /etc/ssl/mtls/ca/cacert.pem \
  -config /etc/ssl/mtls/openssl.cnf  

Сертификат сервера

Сертификат сервера (server.crt) в данном случае, это сертификат который будет закреплен за IP или dns нашего сервиса.

Ключ сервера можно сделать слабее так как он в отличии от CA будет с меньшим сроком жизни

Создание ключа и запроса сервера на подписание нашего сертификата, корневым сертификатом:

sudo openssl req -new -newkey rsa:2048 -nodes \
  -keyout /etc/ssl/mtls/server.key \
  -out /etc/ssl/mtls/server.csr \
  -config /etc/ssl/mtls/openssl.cnf

Теперь подпишем сертификат сервера:

(!) Будет запрос на ввод пароля, того самого который использовался для ключа корневого сертификата.
(!) При заполнении commonName необходимо указать ваш IP (если у вас нет домена) или dns имя которое будет в дальнейшем использоваться. В моем случае я указал *.0q.lol.
Обратите внимание, что используется ключ -extensions v3_req, это указывает на раздел применяемых настроек из нашего openssl.cnf
sudo openssl ca -config /etc/ssl/mtls/openssl.cnf \
  -in /etc/ssl/mtls/server.csr \
  -out /etc/ssl/mtls/server.crt \
  -extensions v3_req -days 365   

Сертификат клиента

Создание ключа и запроса клиента на подписание нашего сертификата, корневым сертификатом:

sudo openssl req -new -newkey rsa:2048 -nodes -keyout /etc/ssl/mtls/client.key \
  -out /etc/ssl/mtls/client.csr \
  -days 365 \
  -config /etc/ssl/mtls/openssl.cnf

Теперь подпишем сертификат клиента:

(!) Будет запрос на ввод пароля, того самого который использовался для ключа корневого сертификата.
(!) При заполнении commonName необходимо имя клиента, например ник.
Обратите внимание, что используется ключ -extensions v3_client, это указывает на раздел применяемых настроек из нашего openssl.cnf
sudo openssl ca -config /etc/ssl/mtls/openssl.cnf \
  -in /etc/ssl/mtls/client.csr \
  -out /etc/ssl/mtls/client.crt \
  -extensions v3_client -days 365

Теперь для удобства использования можно объединить ключ клиента и его сертификат:

(!) При экспорте будет запрошен пароль для шифрования наших данных внутри client.p12, он еще пригодиться при настройке клиента.
Вы так же наверняка заметите странный ключ legacy. Данный ключ нужен, чтобы охватить большее число клиентов, которые смогут понять этот сертификат, например MacOS.
Ключ name в некоторых системах позволяет визуально выделить сертификат среди других.
sudo openssl pkcs12 -export -legacy -in /etc/ssl/mtls/client.crt \
    -inkey /etc/ssl/mtls/client.key -name ClientName \
    -out /etc/ssl/mtls/client.p12

После того как у нас есть client.p12, можно удалить client.crt и client.key

Список отозванных сертификатов (CRL)

Мы должны создадать специальный файл со списком отозванных сертификатов, чтобы сервер мог проверить например, не отозван ли сертификат клиента.

sudo openssl ca -config /etc/ssl/mtls/openssl.cnf \
  -gencrl -out /etc/ssl/mtls/ca/crl.pem  

Цепочка сертификатов (Full Chain)

Для доверия нашему CA нам потребуеться на машине клиента помимо клиентского сертификата, так же установить и цепоцку сертификатов server + CA

Это делается элементарно просто

sudo cat /etc/ssl/mtls/server.crt \
    /etc/ssl/mtls/ca/cacert.pem > /etc/ssl/mtls/full_chain.pem

Отзыв сертификата

Далеко не уходя, чтобы отозвать сертификат, который вам больше не нужен, можно воспользоваться командой:

sudo openssl ca -config /etc/ssl/mtls/openssl.cnf \
  -revoke /etc/ssl/mtls/client.crt 

Если же у нас не сохранился сертификат на сервере, что после конечной настройки будет правильным путем, то можно отозвать по серийному номеру.

Для этого можем посмотреть, какие сертификаты у нас есть в index'е:

sudo vim /etc/ssl/mtls/ca/index.txt

Затем отозвать подставив серийный номер:

sudo openssl ca -revoke -serial <серийный_номер> \
    -config /etc/ssl/mtls/openssl.cnf    

ВАЖНО! Не забудте пересоздать crl.pem и перезапустить nginx

sudo openssl ca -config /etc/ssl/mtls/openssl.cnf \
  -gencrl -out /etc/ssl/mtls/ca/crl.pem
sudo nginx -t && sudo nginx -s reload

Настройка nginx

Теперь настроим nginx для работы с сертификатами и нашим сервисом на http://127.0.0.1:3000

Первым делом отредактируем основной файл конфигурации nginx:

sudo vim /etc/nginx/nginx.conf

В секции http раскомментируем строчку "server_tokens off;"
Данный параметр отвечает за отключение заголовков версии nginx, мы же не хотим чтобы весь интернет знал под какую версию искать CVE =)

Далее создадим конфигурацию для сервиса:

sudo vim /etc/nginx/sites-available/mtls

Заполним файл:

server {
    listen 80;
    server_name mtls.0q.lol;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;

    server_name mtls.0q.lol;
    ssl_certificate     /etc/ssl/mtls/server.crt;
    ssl_certificate_key /etc/ssl/mtls/server.key;
    ssl_client_certificate /etc/ssl/mtls/ca/cacert.pem;
    ssl_verify_client on;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_crl /etc/ssl/mtls/ca/crl.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Создадим ссылку для "включения" конфигурации сервера nginx

sudo ln -s /etc/nginx/sites-available/redirects.conf /etc/nginx/sites-enabled/redirects.conf

Теперь питание компьютера можно отключить можно перезапустить nginx:

sudo nginx -t && sudo nginx -s reload

Настройка клиента

Перемещение на клиента

Для клиента нам потребуется стянуть сертификаты с сервера, сделать это можно например так:

Копируем нужные сертификаты

sudo cp /etc/ssl/mtls/{client.p12,full_chain.pem} ~/
sudo cp /etc/ssl/mtls/ca/cacert.pem ~/

Теперь для просто ты перемещения сожмем

sudo zip -r client.zip client.p12 full_chain.pem cacert.pem
sudo chown <юзер>:<юзер> client.zip

И переместим на свою машину например с помощью scp

scp <юзер>@<хост>:/home/<юзер>/client.zip /<путь куда сохранить>/

Установка на клиенте

На клиенте расспаковываем архив и определяем в чем же мы хотим использовать сертификат.

О чем это я, нам предстоит импортировать сертфикаты и выставить доверие. Для этого необходимо поместить их в хранилище сертификатов, а оно у некоторого ПО свое.

Например, firefox может не работать с системным хранилищем, в то время как chrome работает только с системным.

Поэтому, вынесем настройку firefox отдельно =)

Firefox

Открыв настройки в firefox, воспользуйтесь поиском (так быстрее), введите серт или cert в зависимости от используемой локали.

Если у вас включен third-party root certificates, то сертификаты будут подтягиваться с системного хранилища сертификатов.

Если же такой настройки нет или она по неким соображениям выключена, то открываем просмотр сертификатов
Далее идем во вкладку корневых центров сертификаци и импортируем наш сертификат cacert.pem:

Выставляем доверие для Web

И переходим во вкладку клиентских сертификатов, так же импортируем и вводим пароль от p12:

Все, теперь при переходе на ваш сайт, будет всплывающее окно с выбором сертификата доступа, после подтверждения вы и только вы попадете внутрь.

MacOS

Для настройки нам потребуется запустить Keychain Access

В нем переходим в System - Certificates, после чего в верхнем меню выбираем File - Import Item, и загружаем fullchain.pem

Далее надо установить доверие корневому сертификату, выбираем наш корневой сертификат и открываем его. Далее разворачиваем блок "Trust" и выставляем "Always Trust".

После этого наш второй сертификат (сервера), который так же поместился в хранилище, автоматически станет доверенным.

Ровно таким же образом загружается и client.p12

Думали на этом все? Не тут то было.

В MacOS если вы хотите дать доступ до сертификата приложениям и не хотите каждый раз вводить пароль необходимо проделать еще несколько шагов.

Находим наш клиентский сертификат и раскрываем его. Далее заходим в приватный ключ и переходим в меню Access Control. Тут можно либо выбрать список приложений, которые имеют доступ к ключу и включить/выключить опцию доступа без пароля, либо разрешить всем приложениям иметь доступ к ключу.

Все, теперь браузеры такие как chrome или safari смогут использовать сертификат.

Windows

Тут даже без скриншотов =) Открываем сертификат p12 и импортируем, и импортируем cacert.pem в хранилище доверенных сертификатов.

Burp

Переходим в настройки Burp - Network - TLS - Client TLS Certificates -Add

Указываем домен или IP, кликаем далее и загружаем наш p12

Footer

Предлагайте идеи, буду рад почитать и написать что-нибудь.

Сделать это можно не только в комментарии тут, но и в Telegram канале https://t.me/lazysec