Настройка mTLS для Nginx: клиентские сертификаты
Обеспечьте безопасность панели администратора с помощью mTLS. В статье приведена пошаговая инструкция по созданию CA через openssl и настройке директив ssl_verify_client в Nginx.
Что такое mTLS
Mutual TLS, обычно сокращаемый как mTLS, заставляет Nginx запрашивать у каждого клиента сертификат и отклонять запрос, если этот сертификат отсутствует или не был выдан доверенным центром сертификации (CA), который вы контролируете. Проверка происходит во время TLS-рукопожатия (handshake), поэтому клиент без действительного сертификата даже не доходит до вашего приложения. В этом заключается основное преимущество: панель администратора или эндпоинт с метриками могут быть доступны из публичного интернета без страницы входа и без возможности для бота подобрать пароль.
Реализация проста. Один приватный CA, созданный с помощью openssl, по одному сертификату на пользователя и три директивы в блоке server конфигурации Nginx. Основная работа, определяющая жизнеспособность системы в течение года, заключается в эксплуатации, поэтому большая часть этого руководства посвящена срокам действия, отзыву сертификатов, персональным сертификатам и тому, что делать, если клиенту отказано в доступе, а причина не очевидна.
Две цепочки, а не одна
В настройке mTLS существуют две цепочки сертификатов, и они никак не связаны друг с другом. Объединение их в одну — первая ошибка, которую совершают почти все.
Первая цепочка — серверная. Ваш VPS предъявляет сертификат для admin.example.com, выпущенный публичным центром сертификации (CA), например Let's Encrypt, а браузер проверяет его по корневому хранилищу, которое поставляется вместе с операционной системой. mTLS никак не меняет эту часть. Если certbot выпускает этот сертификат для вас сейчас, оставьте всё как есть: см. выпуск сертификата Let's Encrypt для nginx с помощью certbot.
Вторая цепочка — клиентская. Вы создаете собственный небольшой CA, подписываете по одному сертификату для каждого пользователя, которому нужен доступ, и указываете nginx доверять только этому CA при проверке клиентов. Ни одно публичное корневое хранилище не знает о вашем CA, и это не требуется. Единственная сторона, которая должна ему доверять — это nginx, через файл ssl_client_certificate.
Таким образом, ssl_client_certificate никак не влияет на сертификат, который предъявляет nginx, а цепочка Let's Encrypt никак не влияет на то, какие клиенты получают доступ. Указание ssl_client_certificate на fullchain.pem работает не так, как кажется: эта директива определяет издателей, от которых может быть получен клиентский сертификат, что находится на другом конце соединения. Настройка доверия сервера к вашему CA для его собственных исходящих запросов — это отдельная задача, описанная в добавление собственного CA в хранилище доверенных сертификатов Ubuntu, и системное хранилище доверия — это не то, что nginx считывает при проверке клиента.
Создание собственного клиентского CA с помощью openssl
Размещайте CA отдельно от веб-сервера. Nginx требуется только публичный сертификат CA. Приватный ключ CA используется для подписи новых клиентских сертификатов, поэтому хранение его на сервере, доступном из Интернета, означает, что в случае взлома злоумышленник сможет самостоятельно выпускать валидные клиентские сертификаты.
mkdir -p ~/client-ca/certs ~/client-ca/newcerts ~/client-ca/private ~/client-ca/csr
cd ~/client-ca
chmod 700 private
touch index.txt
echo 1000 > serial
echo 1000 > crlnumberindex.txt, serial и crlnumber представляют собой базу данных CA. openssl ca не запустится без этих файлов. Они также необходимы для последующего отзыва сертификатов, так как список отозванных сертификатов содержит серийные номера, и CA должен хранить информацию о том, какой серийный номер был выдан конкретному клиенту.
Создайте файл ~/client-ca/openssl.cnf. Установите dir в значение полного пути к этому каталогу, так как openssl ca не выполняет раскрытие ~.
[ ca ]
default_ca = client_ca
[ client_ca ]
dir = /home/you/client-ca
database = $dir/index.txt
new_certs_dir = $dir/newcerts
certificate = $dir/ca.crt
private_key = $dir/private/ca.key
serial = $dir/serial
crlnumber = $dir/crlnumber
default_md = sha256
default_days = 365
default_crl_days = 30
policy = policy_loose
rand_serial = no
unique_subject = no
email_in_dn = no
[ policy_loose ]
commonName = supplied
countryName = optional
stateOrProvinceName = optional
organizationName = optional
organizationalUnitName = optional
emailAddress = optional
[ client_ext ]
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuerТеперь создайте ключ CA и его самоподписанный сертификат:
openssl genrsa -aes256 -out private/ca.key 4096
chmod 600 private/ca.key
openssl req -x509 -new -key private/ca.key -sha256 -days 3650 \
-subj "/O=Example Ops/CN=Example Ops Client CA" \
-addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
-addext "keyUsage=critical,keyCertSign,cRLSign" \
-out ca.crt-aes256 устанавливает парольную фразу для ключа CA, поэтому при каждом процессе подписи система будет запрашивать её ввод. В этом и заключается смысл защиты. Проверьте созданные файлы:
openssl x509 -in ca.crt -noout -subject -dates -ext basicConstraintsВ поле subject должен быть указан ваш CA, а срок действия должен составлять десять лет. Строка расширений должна содержать CA:TRUE, pathlen:0. pathlen:0 означает, что данный CA может подписывать конечные сертификаты, но не может подписывать другие CA. Это ограничивает глубину цепочки ровно одним уровнем и позволяет оставить ssl_verify_depth без изменений.
Выпуск клиентского сертификата для каждого пользователя
Один сертификат на одного человека. Никогда не используйте общий сертификат для всей команды, так как его невозможно отозвать, не заблокировав доступ всем сразу, и он не позволяет идентифицировать конкретного пользователя.
openssl genrsa -out private/alice.key 2048
openssl req -new -key private/alice.key -out csr/alice.csr \
-subj "/O=Example Ops/CN=alice"
openssl ca -config openssl.cnf -extensions client_ext \
-days 365 -notext -in csr/alice.csr -out certs/alice.crtopenssl ca выводит сертификат, который собирается подписать, запрашивает парольную фразу CA, просит подтверждения, а затем добавляет строку в index.txt. Используйте -batch при автоматизации процесса скриптами. Секция client_ext важна из-за одной строки в ней: extendedKeyUsage = clientAuth. Сертификат, в расширенном использовании ключа (extended key usage) которого указано только serverAuth, будет отклонен как непригодный для клиентской аутентификации, поэтому всегда явно указывайте назначение.
Перед передачей сертификата проверьте его соответствие CA:
openssl verify -CAfile ca.crt certs/alice.crtЭта команда выводит certs/alice.crt: OK. Любой другой вывод означает, что сертификат и CA не совпадают, и никакая конфигурация nginx не исправит эту проблему.
Объедините ключ и сертификат в один файл, который можно импортировать в браузер:
openssl pkcs12 -export -inkey private/alice.key -in certs/alice.crt \
-name "alice at example ops" -out alice.p12При экспорте потребуется пароль, который защищает файл при передаче. Отправляйте файл и пароль по разным каналам связи и передавайте пользователям .p12, а не просто .key. Вы можете добавить -certfile ca.crt, чтобы включить CA в пакет, но nginx это не требуется: nginx уже содержит ca.crt, поэтому сертификат, подписанный непосредственно этим CA, проходит проверку самостоятельно.
OpenSSL 3, который поставляется в Ubuntu 24.04, создает файлы PKCS#12 с использованием современных алгоритмов шифрования, которые поддерживаются браузерами и операционными системами по состоянию на август 2026 года. Если старое ПО для импорта не принимает файл, выполните экспорт повторно с добавлением -legacy — это задействует более старые алгоритмы, ожидаемые импортером. Перед использованием этого флага ознакомьтесь с сообщением об ошибке, которое выдает импортер.
Настройка nginx с использованием ssl_client_certificate и ssl_verify_client
Скопируйте сертификат центра сертификации (CA), и только его, на сервер.
scp ca.crt user@admin.example.com:/tmp/client-ca.crt
ssh user@admin.example.com \
'sudo install -o root -g root -m 644 /tmp/client-ca.crt /etc/nginx/client-ca.crt'Права доступа 644 здесь корректны. Сертификат CA является публичной информацией. Закрытый ключ CA должен оставаться на вашей рабочей станции.
Затем добавьте три директивы в блок server, который уже выполняет TLS termination:
server {
listen 443 ssl;
server_name admin.example.com;
ssl_certificate /etc/letsencrypt/live/admin.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/admin.example.com/privkey.pem;
ssl_client_certificate /etc/nginx/client-ca.crt;
ssl_verify_client on;
ssl_verify_depth 1;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Значение ssl_verify_depth 1 является стандартным для nginx и означает, что клиентский сертификат должен быть подписан непосредственно CA из этого файла. Увеличивайте его только в том случае, если вы добавляете промежуточный сертификат. Во время рукопожатия (handshake) nginx также отправляет клиенту список имен субъектов из ssl_client_certificate; именно так браузер понимает, какой из своих сертификатов предложить. По этой причине следует использовать ssl_client_certificate, а не ssl_trusted_certificate, который выполняет проверку аналогичным образом, но не отправляет список имен.
В Ubuntu 24.04 поставляется nginx 1.24, где поддержка HTTP/2 указывается в строке listen как listen 443 ssl http2;. Начиная с версии nginx 1.25.1, этот формат считается устаревшим, и для HTTP/2 введена отдельная директива http2 on;. Выбор варианта не влияет на проверку сертификата.
Перезагрузите конфигурацию и проверьте результат:
sudo nginx -t && sudo systemctl reload nginx
curl -i https://admin.example.com/Команда nginx -t выводит syntax is ok и test is successful. Вызов curl выполняется без сертификата, поэтому он должен вернуть 400 Bad Request с телом ответа No required SSL certificate was sent. Это означает, что nginx отклоняет запрос на уровне шлюза, конфигурация активна, и запрос не был передан приложению. Теперь попробуйте выполнить запрос корректно:
curl --cert certs/alice.crt --key private/alice.key https://admin.example.com/Этот запрос должен вернуть ответ, который предоставляет ваше приложение.
Почему директива должна находиться в блоке server
Сертификат передается во время TLS-рукопожатия еще до того, как nginx прочитает строку запроса, поэтому в этот момент nginx не знает, в какой location попадет запрос. Размещение ssl_verify_client on; внутри location заставляет клиента выполнять повторное согласование (renegotiation) в середине соединения. Протокол TLS 1.3 исключил возможность повторного согласования, а HTTP/2 запрещает его, поэтому в современном стеке такая конфигурация приводит к ошибке вместо запроса сертификата.
Выполняйте ограничение области действия самостоятельно. Запрашивайте сертификат на уровне server, а затем принимайте решение для каждого location:
ssl_verify_client optional;
location /metrics {
if ($ssl_client_verify != SUCCESS) { return 403; }
proxy_pass http://127.0.0.1:9090;
}
location /healthz {
proxy_pass http://127.0.0.1:8080;
}$ssl_client_verify содержит SUCCESS, либо NONE, если клиент ничего не отправил, либо FAILED: с указанием причины. С параметром optional nginx запрашивает сертификат и проверяет его только в том случае, если он был предоставлен. Именно это позволяет публичному пути /healthz работать, в то время как /metrics остается закрытым. Если сертификат отправлен, но не прошел проверку, nginx отклонит его на этом этапе. Если вы хотите самостоятельно проверять не прошедший проверку сертификат, используйте optional_no_ca, и тогда ваша собственная проверка должна интерпретировать любое значение, отличное от SUCCESS, как отказ.
В nginx для этого существуют нестандартные коды состояния, и error_page может перехватывать их, чтобы пользователь, которому отказано в доступе, получил объяснение, а не просто ошибку 400:
error_page 495 496 = @needcert;
location @needcert {
default_type text/plain;
return 200 "This host requires a client certificate. Ask ops for one.\n";
}Код 495 означает, что клиентский сертификат не прошел проверку. Код 496 означает, что клиент не предоставил сертификат. Оформляйте эту страницу в виде простого текста, так как у пользователя, который ее читает, нет активной сессии и учетной записи.
Как установить клиентский сертификат в браузере?
Firefox использует собственное хранилище сертификатов: перейдите в Settings, затем в Privacy and Security, выберите View Certificates, перейдите на вкладку Your Certificates, нажмите Import и выберите .p12, после чего введите пароль от него.
Chrome и Edge используют системное хранилище в Windows и macOS, поэтому открытие файла .p12 запускает мастер импорта операционной системы. В Linux Chrome считывает отдельную базу данных NSS (network security services) в домашнем каталоге пользователя, поэтому надежнее всего использовать инструмент командной строки:
sudo apt install -y libnss3-tools
pk12util -d sql:$HOME/.pki/nssdb -i alice.p12После этого откройте сайт, и браузер предложит выбрать сертификат для отправки. Chrome запоминает этот выбор до конца сессии, поэтому для повторного выбора сертификата необходимо перезапустить браузер. Сертификат привязан к одному профилю браузера на одном устройстве: сертификат, импортированный в Firefox, будет недоступен в Chrome, и оба они будут недоступны на мобильном телефоне.
Тестирование с помощью curl --cert
Используйте curl для отладки, так как эта утилита предоставляет подробный отчет о своих действиях.
curl -v --cert certs/alice.crt --key private/alice.key https://admin.example.com/Вы можете объединить сертификат и ключ в один PEM-файл и передать его через --cert alice.pem. Если ключ защищен парольной фразой, curl запросит её. Также можно использовать --cert alice.pem:passphrase, но тогда пароль сохранится в истории командной оболочки, поэтому лучше дождаться запроса.
Перед тем как искать проблему в Nginx, стоит выполнить две проверки. Во-первых, сертификат и ключ должны составлять пару:
openssl x509 -noout -pubkey -in certs/alice.crt | openssl sha256
openssl pkey -pubout -in private/alice.key | openssl sha256Два одинаковых хеша означают, что файлы соответствуют друг другу. Разные хеши означают, что вы перепутали файлы, но ни один клиент не сообщит вам об этой причине напрямую.
Во-вторых, сервер должен запрашивать ваш CA:
openssl s_client -connect admin.example.com:443 -servername admin.example.com </dev/nullНайдите блок Acceptable client certificate CA names в выводе и проверьте наличие в нем subject вашего CA. Если этот блок отсутствует, значит, Nginx не запрашивает сертификат в том server block, который ответил на запрос. Вероятно, ваши директивы попали в другой блок, чаще всего в default server.
Передача CN клиента приложению
Сертификат содержит информацию о том, кто выполнил вызов, но приложение за прокси-сервером не видит уровень TLS, поэтому nginx должен передать имя самостоятельно.
map $ssl_client_s_dn $client_cn {
default "";
"~,?CN=(?<cn>[^,]+)" $cn;
}$ssl_client_s_dn содержит distinguished name субъекта в формате RFC 2253, который выглядит как CN=alice,O=Example Ops. Директива map извлекает поле CN в переменную $client_cn. Используйте в качестве CN простое имя пользователя, так как запятая внутри CN в этом формате экранируется, а приведенное выше регулярное выражение не обрабатывает экранирование.
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Client-Cert-CN $client_cn;
proxy_set_header X-Client-Cert-Serial $ssl_client_serial;
}proxy_set_header заменяет любой заголовок с таким именем, отправленный вызывающей стороной, поэтому никто не сможет подделать X-Client-Cert-CN через эту локацию. Это обеспечивается двумя условиями. nginx наследует proxy_set_header с внешнего уровня только в том случае, если на внутреннем уровне не определено ни одного собственного заголовка, поэтому вторая локация с одной строкой proxy_set_header молча отменяет все заголовки, установленные выше, включая этот. Кроме того, приложение должно быть недоступно напрямую, минуя nginx, что означает привязку его к 127.0.0.1, а не к 0.0.0.0, так как приложение на публичном порту будет считывать поддельный заголовок прямо из интернета. Сторона прокси-сервера для этого случая описана в построчном разборе конфигурации обратного прокси-сервера nginx. Если приложению требуется весь сертификат целиком, а не только имя, $ssl_client_escaped_cert передаст его в URL-кодированном и безопасном виде внутри заголовка.
Как отозвать один клиентский сертификат?
Сотрудник увольняется или ноутбук теряется. Вы отзываете один конкретный сертификат, а остальные продолжают работать — именно поэтому для каждого пользователя выпускается отдельный сертификат.
cd ~/client-ca
openssl ca -config openssl.cnf -revoke certs/alice.crt
openssl ca -config openssl.cnf -gencrl -out crl.pemПервая команда меняет статус записи с серийным номером в index.txt с V на R. Вторая команда создает список отзыва сертификатов (CRL) — подписанный файл, содержащий отозванные серийные номера. Скопируйте его на сервер и укажите путь к нему в nginx с помощью ssl_crl /etc/nginx/client-ca.crl; рядом с остальными директивами.
scp crl.pem user@admin.example.com:/tmp/client-ca.crl
ssh user@admin.example.com 'sudo install -m 644 /tmp/client-ca.crl /etc/nginx/client-ca.crl && sudo nginx -t && sudo systemctl reload nginx'Здесь кроется ловушка, которая заблокирует доступ для всех. CRL имеет дату nextUpdate, которая задается параметром default_crl_days (в примере выше установлено значение 30). Как только эта дата проходит, OpenSSL считает список устаревшим и отклоняет проверку для любого клиентского сертификата с ошибкой CRL has expired, а не только для отозванного. nginx считывает файл при загрузке конфигурации, поэтому обновление файла на диске не даст эффекта до выполнения reload. Обновляйте и перезагружайте CRL по расписанию, которое укладывается в этот интервал (например, еженедельно при 30-дневном сроке действия), и проверяйте даты перед копированием:
openssl crl -in crl.pem -noout -lastupdate -nextupdateДля небольшого числа пользователей есть более простой вариант. Поскольку CA принадлежит вам, nginx может напрямую отклонить серийный номер, не используя механизмы CRL:
map $ssl_client_serial $revoked {
default 0;
"1002" 1;
}Используйте это вместе с if ($revoked) { return 403; } в блоке location. У этого метода нет срока действия, о котором нужно помнить. Однако этот список не передается другим системам, поэтому любые другие сервисы, доверяющие вашему CA, не будут знать об отзыве. Для одного экземпляра nginx перед одним приложением это самое надежное и простое решение. Переходите на CRL, когда точек входа станет больше одной.
Какой срок действия должен быть у клиентских сертификатов?
Устанавливайте срок действия клиентских сертификатов в один год или меньше, если вы готовы к регулярному перевыпуску. Истечение срока действия — это скрытая проблема, так как пользователя никто не предупреждает заранее. В один день пользователь открывает панель управления, Nginx отклоняет соединение, а браузер сообщает об ошибке своими словами, в которых редко упоминается истечение срока действия. Срок действия CA установите в 10 лет и запишите дату истечения там, где вы её точно увидите. Если сертификат CA станет недействительным, все выданные им сертификаты перестанут проходить проверку в тот же день.
Две команды помогут вам контролировать ситуацию:
openssl x509 -in certs/alice.crt -noout -subject -serial -enddate
awk -F'\t' '{print $1, $2, $4}' ~/client-ca/index.txtПервый столбец в index.txt показывает статус: V для действующих, R для отозванных, E для просроченных. Второй столбец содержит дату истечения в формате YYMMDDHHMMSSZ, а четвёртый — серийный номер. Этот файл является единственным реестром выданных сертификатов, поэтому создавайте его резервные копии вместе с ключом CA и относитесь к ним как к конфиденциальным данным.
Продление — это выпуск нового сертификата, а не продление старого. Сгенерируйте новый ключ и CSR (certificate signing request), подпишите его, передайте пользователю, а затем отзовите старый сертификат, как только пользователь подтвердит работоспособность нового.
От чего защищает mTLS, а от чего нет
mTLS исключает неавторизованный доступ. Сканер, обнаруживший ваш хост, получает отказ еще на этапе рукопожатия (handshake). Он не отправляет HTTP-запрос, не видит форму входа и не может проверить украденные пароли. Атаки методом перебора учетных данных (credential stuffing) становятся невозможны. Уязвимости в механизме авторизации приложения остаются недоступными для любого, у кого нет сертификата. Также mTLS устраняет проблему общих секретов, которые пользователи пересылают в чатах: закрытый ключ — это файл, который сложно скопировать случайно.
mTLS не защищает от скомпрометированного клиента. Вредоносное ПО на ноутбуке получит доступ к файлу ключа и парольной фразе в тот момент, когда владелец её введет. Для сервера такой злоумышленник выглядит как легитимный пользователь, так как сертификат подтверждает владение файлом, а не присутствие человека. .p12 пароли и полнодисковое шифрование по-прежнему важны.
Это также не является авторизацией. Любой валидный сертификат дает доступ ко всему, что обслуживает данный серверный блок, если вы не проверяете $client_cn и не предпринимаете действий на основе этого значения. По умолчанию два владельца сертификатов имеют идентичные права доступа.
mTLS защищает только путь через Nginx. Если приложение также слушает публичный порт, mTLS перед ним — лишь декорация. Привязывайте приложение к 127.0.0.1 и держите закрытым его порт в межсетевом экране. Другой вход в ту же систему — SSH, он требует такого же внимания, что описано в укреплении SSH-доступа на вашем VPS.
Последнее ограничение, с которым вы столкнетесь в день включения: всё, что не может предоставить сертификат, перестанет работать. Это касается мониторов доступности (uptime monitors), вебхуков от платежных систем, RSS-ридеров и мобильных приложений без доступа к хранилищу сертификатов. Решите вопрос с ними до того, как установите ssl_verify_client on, так как сбой будет полным и, с их стороны, незаметным.
Когда клиенту отказано в доступе, изучите отчет клиента
Сообщение, которое выдает получивший отказ клиент, зависит от браузера, версии curl и используемой библиотеки TLS. Поэтому ориентируйтесь на вывод вашего клиента, а не на сообщения из сторонних источников. Полезная информация находится на сервере.
sudo tail -n 50 /var/log/nginx/error.logОтклоненный сертификат оставляет в логе строку, содержащую client SSL certificate verify error, за которой следует причина, указанная OpenSSL. Именно на эту причину и нужно реагировать. Обычно это одна из нескольких проблем. Сертификат выдан другим центром сертификации (CA), нежели тот, что указан в файле ssl_client_certificate. Срок действия сертификата истек или еще не наступил. Срок действия CRL на сервере прошел nextUpdate, поэтому теперь он отклоняет всех клиентов, а не одного конкретного.
Если браузер вообще не предлагает сертификат, проблема возникла раньше этапа проверки. nginx отправляет список допустимых имен издателей во время рукопожатия (handshake), и если браузер не нашел в своем хранилище ничего подходящего, ему нечего вам предложить. Импортируйте .p12 повторно в тот профиль, который вы используете для просмотра.
Стоит упомянуть еще один случай. Если вы проводили тестирование с одиночным самоподписанным клиентским сертификатом, а не с сертификатом, подписанным вашим CA, проверка не будет пройдена. nginx проверяет подпись по файлу CA, а самоподписанный сертификат в нем отсутствует. Механика создания сертификата такая же, как в создании самоподписанного сертификата в Ubuntu. Для mTLS просто требуется дополнительный шаг, на котором ваш CA подписывает этот сертификат.
FAQ
Нужен ли мне сертификат Let's Encrypt, если я использую mTLS?
Да. Эти два сертификата никак не связаны. Ваш сервер предъявляет собственный сертификат, чтобы браузер доверял имени хоста, и этот сертификат должен быть выдан центром сертификации (CA), которому браузер уже доверяет. Ваш клиентский CA — это отдельная закрытая цепочка, используемая только для проверки того, кто именно подключается. Настройка ssl_client_certificate никак не влияет на сертификат, который предъявляет nginx, и она не должна указывать на вашу цепочку Let's Encrypt.
Почему браузер не предлагает мне выбрать сертификат?
Во время рукопожатия nginx отправляет список допустимых издателей, сформированный на основе файла в ssl_client_certificate. Браузер предлагает только те сертификаты, издатель которых есть в этом списке. Отсутствие запроса означает, что в браузере нет сертификатов от вашего CA: либо сертификат был импортирован в другой профиль браузера, либо он был подписан другим CA, а не тем, который установлен на сервере. Выполните openssl s_client -connect admin.example.com:443 и найдите в выводе имена допустимых клиентских CA, чтобы увидеть, какой именно CA запрашивает сервер.
Можно ли требовать клиентский сертификат только для одного URL?
Нет, если использовать ssl_verify_client on внутри location. Сертификат передается во время рукопожатия, до того как nginx узнает путь запроса, а механизм пересогласования (renegotiation), который мог бы решить эту задачу, отсутствует в TLS 1.3 и запрещен в HTTP/2. Установите ssl_verify_client optional; в блоке server, затем в каждой защищаемой location проверяйте $ssl_client_verify и возвращайте 403, если значение не равно SUCCESS.
Как отозвать доступ для одного пользователя?
Отозвите этот сертификат с помощью openssl ca -revoke, пересоздайте список с помощью openssl ca -gencrl, скопируйте его на сервер и перезагрузите nginx, чтобы он прочитал новый файл. На остальных пользователей это не повлияет, но только при условии, что у каждого человека свой сертификат, а не один общий. Следите за датой nextUpdate в CRL, так как просроченный CRL приводит к сбою проверки для всех клиентов, а не только для отозванных.
Заменяет ли mTLS страницу входа?
В плане доступности — да: без сертификата запрос вообще не доходит до приложения, поэтому нет формы для атаки и пароля, который можно подобрать. В плане идентификации внутри приложения — нет. Сертификат лишь доказывает, что у вызывающей стороны есть файл ключа, поэтому украденный ноутбук будет считаться валидным пользователем. Передавайте CN выше по цепочке, сохраняйте все учетные записи и права доступа, которые уже есть в приложении, и рассматривайте сертификат как шлюз перед ними.