Настройка mTLS в Nginx: конфигурация, проверка клиентских сертификатов и диагностика ошибок | AdminWiki

Настройка mTLS в Nginx: конфигурация, проверка клиентских сертификатов и диагностика ошибок

11 сентября 2026 10 мин. чтения

Взаимная TLS-аутентификация в Nginx включается тремя директивами в server block: ssl_client_certificate с путём к файлу CA, ssl_verify_client on и ssl_verify_depth 2. После перезагрузки запрос без валидного клиентского сертификата получает 400 Bad Request, а запрос с сертификатом, подписанным вашим CA, проходит к бэкенду.

Дальше весь цикл целиком: генерация корневого CA, выпуск серверного и клиентского сертификатов через openssl, рабочий конфиг, проверка через curl и openssl s_client, плановая ротация, отзыв через CRL и разбор кодов 400, 495 и 496 в error.log.

Что такое mTLS и зачем включать его в Nginx

Обычный TLS решает одну задачу: клиент убеждается, что сервер тот, за кого себя выдаёт. Сервер клиента не проверяет вообще.

mTLS (mutual TLS) добавляет встречную проверку. Клиент присылает свой сертификат прямо в рукопожатии, Nginx сверяет подпись с указанным CA, срок действия и цепочку, и только потом пропускает HTTP-запрос.

За это отвечает модуль ngx_http_ssl_module. Три директивы задают всю логику: ssl_client_certificate указывает файл с доверенными CA (или bundle из нескольких), ssl_verify_client включает проверку, ssl_verify_depth ограничивает длину цепочки промежуточных сертификатов.

Отличия mTLS от одностороннего TLS

Односторонний TLS: клиент получает server.crt, проверяет SAN, срок и подпись CA, согласует ключ сессии и работает дальше. Сервер на этом этапе видит анонимного гостя. Доступ ограничивают другими средствами: паролем, токеном, фильтром по IP.

mTLS: сервер в сообщении CertificateRequest просит клиента предъявить сертификат. Клиент отправляет client.crt и отдельным сообщением CertificateVerify доказывает владение приватным ключом. Nginx проверяет цепочку до доверенного CA, срок и статус отзыва. Рукопожатие завершается только после успешной проверки.

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

Когда mTLS в Nginx действительно нужен

  • Внутренние API между микросервисами: сервис A обращается к сервису B, обе стороны доказывают подлинность. Базовая схема zero-trust, где сеть не считается доверенной.
  • Kubernetes ingress: mTLS на входном контроллере закрывает служебные эндпоинты, а клиентские сертификаты выпускает cert-manager.
  • Prometheus, Grafana, админ-панели: вместо basic auth конкретному инженеру или боту выдаётся сертификат, а доступ ограничивается по CN.
  • IoT и периферийные устройства: каждое устройство получает уникальный сертификат, Nginx различает их по CN и серийному номеру.
  • Партнёрские интеграции: контрагент получает сертификат вашего CA и доступ ровно к одному location.
  • Шлюзы к внешним API нейросетей: mTLS прикрывает внутренний прокси, через который уходят платёжные ключи, например при построении доступа к моделям через AiTunnel.

Для публичного сайта с обычными посетителями mTLS избыточен. Выдача сертификата каждому пользователю ломает UX, добавляет поддержку и не даёт выигрыша по сравнению с HTTPS и токенами.

Выпуск сертификатов: CA, серверный и клиентский

Вся цепочка собирается локально через openssl. Серверу нужны четыре файла, каждому клиенту три. Приватный ключ CA держите офлайн: на машине без сетевого доступа как минимум вне веб-сервера. Если планируете хранить сертификаты в облаке и раздавать их сервисам, заранее продумайте, где лежит ca.key, отдельно от самого Nginx, например на отдельном VDS, который вы арендуете под инфраструктуру, скажем через Timeweb Cloud.

Создание корневого CA

openssl genrsa -out ca.key 4096
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt -subj '/C=RU/O=Example/CN=Example Root CA'

Срок 3650 дней для корневого CA выбран сознательно. Перевыпуск CA означает перевыпуск всех клиентских сертификатов разом. Права на ca.key выставьте в 600 и не копируйте его на прод-сервер: Nginx нужен только ca.crt.

Серверный сертификат с SAN

openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr -subj '/C=RU/O=Example/CN=api.example.com'

cat > server.ext <<'EOF'
basicConstraints=CA:FALSE
keyUsage=digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth
subjectAltName=DNS:api.example.com,DNS:www.api.example.com,IP:10.0.0.10
EOF

openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 825 -sha256 -extfile server.ext

Отсутствие subjectAltName даёт ошибку hostname mismatch уже при проверке клиентом. Браузеры и curl игнорируют CN, если в сертификате нет SAN. Вносите в SAN все имена, по которым сервис доступен: DNS-имена и IP-адреса. Срок 825 дней согласуется с лимитом Apple и Chrome для публичных сертификатов и подходит для внутренних.

Клиентский сертификат

openssl genrsa -out client.key 2048
openssl req -new -key client.key -out client.csr -subj '/C=RU/O=Example/CN=service-a'

cat > client.ext <<'EOF'
basicConstraints=CA:FALSE
keyUsage=digitalSignature
extendedKeyUsage=clientAuth
subjectAltName=DNS:service-a
EOF

openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 365 -sha256 -extfile client.ext

Ключевое отличие в extendedKeyUsage: clientAuth вместо serverAuth. CN задаёт идентификатор клиента, по нему потом строится авторизация на бэкенде. Проверьте результат до правок в конфиге Nginx:

openssl verify -CAfile ca.crt server.crt
openssl verify -CAfile ca.crt client.crt
openssl x509 -in client.crt -noout -subject -issuer -dates

Автоматическое продление SAN-сертификатов в Kubernetes и работа с массовыми выпусками разобраны в практическом руководстве по SSL/TLS для сисадминов.

ФайлГде хранитсяНазначение
ca.keyОфлайн, вне сервераПодпись всех сертификатов, компрометация обнуляет доверие
ca.crt/etc/nginx/tls/Публичный сертификат CA для проверки клиентов
server.key/etc/nginx/tls/Приватный ключ сервера, права 600
server.crt/etc/nginx/tls/Серверный сертификат с SAN, отдаётся клиентам
client.keyУ клиентаДоказательство владения, не передаётся по сети
client.crtУ клиентаПредъявляется Nginx в рукопожатии

Конфигурация Nginx для mTLS

Базовый server block с ssl_verify_client

server {
    listen 443 ssl;
    server_name api.example.com;

    ssl_certificate     /etc/nginx/tls/server.crt;
    ssl_certificate_key /etc/nginx/tls/server.key;

    ssl_client_certificate /etc/nginx/tls/ca.crt;
    ssl_verify_client on;
    ssl_verify_depth 2;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    location /api/ {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header X-SSL-Client-DN $ssl_client_s_dn;
        proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
        proxy_set_header X-SSL-Client-Serial $ssl_client_serial;
        proxy_set_header X-SSL-Client-Fingerprint $ssl_client_fingerprint;
    }
}

Проверьте синтаксис и перезагрузите конфиг без разрыва активных соединений:

nginx -t && systemctl reload nginx

Если в ca.crt лежит несколько CA, Nginx примет любой из них. Формат файла простой: конкатенация PEM-блоков без лишних разделителей. Клиент тоже может прислать не один файл, а цепочку client.crt плюс промежуточный сертификат, тогда ssl_verify_depth 2 разрешает одно промежуточное звено.

ДирективаЗначениеЧто делает
ssl_client_certificateпуть к ca.crt или bundleНабор доверенных CA для проверки клиента
ssl_verify_clienton / optional / optional_no_ca / offРежим проверки клиентского сертификата
ssl_verify_depth1-3Максимальная длина цепочки промежуточных CA
ssl_crlпуть к crl.pemСписок отозванных сертификатов, читается при старте
ssl_session_cacheshared:SSL:10mКэш сессий, ускоряет повторные подключения

Режимы ssl_verify_client: on, optional, optional_no_ca

РежимПоведениеКогда использовать
offКлиентский сертификат не запрашиваетсяПубличный HTTPS без ограничений
onСертификат обязателен, цепочка проверяетсяПрод: API, админки, сервис-сервис
optionalСертификат не обязателен, но присланный проверяетсяПереходный период, два способа доступа
optional_no_caСертификат принимается без проверки подписи CAТолько отладка, в прод не ставить

Режим optional_no_ca опасен тем, что Nginx пропустит самоподписанный сертификат с произвольным CN. Соединение установится, бэкенд получит данные от неаутентифицированного клиента. Переменная $ssl_client_verify покажет SUCCESS только для корректной цепочки, но использовать её как единственную защиту поздно: подключение уже состоялось.

Передача данных клиента в upstream

Nginx отдаёт параметры сертификата в переменных, которые пробрасываются заголовками. Бэкенд читает CN, серийный номер и результат проверки, строит по ним авторизацию и пишет их в аудит-лог.

ПеременнаяСодержимоеИспользование
$ssl_client_s_dnSubject DN клиентаАвторизация по CN, логирование доступа
$ssl_client_verifySUCCESS, FAILED или NONEОтсечение неаутентифицированных запросов в режиме optional
$ssl_client_serialСерийный номер сертификатаТочный отзыв конкретного экземпляра
$ssl_client_fingerprintSHA1-отпечатокПривязка ключа внутри приложения

Проверка mTLS через curl и openssl

Успешный запрос с клиентским сертификатом

curl -v --cert client.crt --key client.key --cacert ca.crt https://api.example.com/api/health

Флаг --cacert указывает, каким CA проверять серверный сертификат, --cert и --key передают клиентскую пару. Ожидаемый ответ 200 OK. В verbose-выводе ищите строку SSL certificate verify ok, а если бэкенд возвращает заголовки, смотрите X-SSL-Client-DN в ответе.

curl -s -o /dev/null -w '%{http_code}' --cert client.crt --key client.key --cacert ca.crt https://api.example.com/api/health

Эквивалент через openssl s_client удобен, когда нужно разобрать само рукопожатие, а не HTTP-обмен:

openssl s_client -connect api.example.com:443 -cert client.crt -key client.key -CAfile ca.crt -servername api.example.com -state

В выводе проверьте Verify return code: 0 (ok) и убедитесь, что клиентский сертификат действительно ушёл на сервер. Если сертификат не отправлен, TLS-сессия может установиться, но Nginx отклонит запрос на уровне HTTP.

Запрос без сертификата и с неверным CA

curl -v --cacert ca.crt https://api.example.com/api/health

Без клиентского сертификата при ssl_verify_client on ответ 400 Bad Request. С сертификатом от чужого CA рукопожатие обрывается или завершается тем же 400, зависит от момента сбоя. Таблица кодов помогает не гадать:

КодПричинаЧто проверить
200Цепочка клиента валиднаПрава и CN на стороне бэкенда
400Сертификат не предъявлен или не прошёл проверкуНаличие --cert и --key, содержимое error.log
403Сертификат валиден, но доступ запрещён приложениемЛогику авторизации по CN в бэкенде
495Внутренний код: ошибка проверки клиентского сертификатаПуть ssl_client_certificate, срок и подпись CA
496Внутренний код: сертификат не предоставленКлиент не отправил cert или запрос идёт на другой порт
502Бэкенд недоступенproxy_pass и состояние upstream

Коды 495 и 496 Nginx не отдаёт наружу по умолчанию: клиент видит 400. Чтобы получить их в ответе, добавьте error_page 495 496 = @mtls_error и верните из этого location свой JSON с описанием проблемы.

Если схема с самоподписанным CA кажется громоздкой, сравните её с ручной установкой обычного сертификата в пошаговом руководстве по установке SSL на Nginx и Apache.

Ротация и отзыв клиентских сертификатов

Плановая ротация без простоя

Порядок из пяти шагов: 1) выпустить новый CA; 2) собрать ca-bundle.crt из старого и нового ca.crt; 3) указать bundle в ssl_client_certificate и перезагрузить Nginx; 4) перевыпустить клиентские сертификаты от нового CA; 5) удалить старый CA из bundle и снова перезагрузить.

cat ca-old.crt ca-new.crt > ca-bundle.crt
nginx -t && systemctl reload nginx

На шаге 3 работают оба CA. На шаге 5 остаётся только новый, поэтому перед последней перезагрузкой посмотрите логи и метрики: записи с FAILED в $ssl_client_verify покажут, кто ещё ходит со старым сертификатом. Если счётчик не нулевой, пауза ещё на сутки дешевле, чем разбор инцидента с отвалившимся сервисом.

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

openssl ca -config openssl.cnf -revoke client.crt
openssl ca -config openssl.cnf -gencrl -out crl.pem
openssl crl -in crl.pem -noout -text

Директива ssl_crl подключает список отозванных сертификатов к конфигу:

ssl_crl /etc/nginx/tls/crl.pem;
ssl_verify_client on;

Файл читается при старте воркеров, поэтому после каждой генерации CRL нужен reload. Ограничение: для парка в тысячи устройств CRL растёт и замедляет каждое рукопожатие. Там уместнее короткие сроки жизни сертификатов с частой ротацией либо проверка отзыва на стороне приложения.

Диагностика ошибок mTLS в Nginx

Коды 400, 495, 496: что означают

400 Bad Request в mTLS почти всегда означает одно из двух: клиент не отправил сертификат при ssl_verify_client on, либо цепочка не сошлась с ssl_client_certificate. Коды 495 и 496 внутренние, они появляются в логах и в директиве error_page, наружу уходит 400, пока вы не зададите свою обработку.

Чтение error.log и типичные сообщения

tail -f /var/log/nginx/error.log

Сообщения, по которым причина видна сразу:

  • client SSL certificate verify error: (18:unable to get local issuer certificate) - клиентский сертификат подписан CA, которого нет в ssl_client_certificate.
  • client SSL certificate verify error: (10:certificate has expired) - истёк срок клиентского сертификата.
  • client sent no required SSL certificate while reading client request headers - запрос пришёл без сертификата в режиме on.
  • SSL_do_handshake() failed (SSL: error:...) - сбой на уровне TLS, чаще из-за несовпадения протоколов, кривого ключа или обрыва балансировщиком.

Чек-лист диагностики

  1. Проверить значение ssl_verify_client: режим on требует сертификат от каждого запроса.
  2. Проверить путь в ssl_client_certificate и права на чтение файла у пользователя, от которого работает Nginx.
  3. Проверить срок, SAN и расширение clientAuth у клиентского сертификата.
  4. Проверить цепочку командой openssl verify -CAfile ca.crt client.crt.
  5. Сопоставить код verify error из error.log с таблицей ниже.
СимптомПричинаРешение
400 при запросе с сертификатомСертификат подписан другим CAДобавить нужный CA в ssl_client_certificate или перевыпустить сертификат
400 без сертификатаАктивен ssl_verify_client onПередать --cert и --key либо временно включить optional
verify error 18Издатель не найденСобрать полную цепочку, проверить bundle и промежуточный CA
verify error 10Истёк срок действияПеревыпустить сертификат, поставить мониторинг дат через openssl x509 -checkend
496 в логеКлиент не прислал сертификатПроверить настройки клиента и балансировщик перед Nginx
Локально работает, снаружи нетБалансировщик обрывает TLSПроксировать mTLS сквозным способом или передавать данные сертификата в заголовках

Безопасность и производительность mTLS

Рекомендуемые параметры TLS

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;

TLS 1.2 оставьте для старых клиентов, TLS 1.3 включите обязательно. Шифры aNULL и MD5 отключите, иначе проверка сертификата теряет смысл. Полный набор настроек HTTPS с HSTS и редиректами собран в руководстве по SSL/TLS и HTTPS в Nginx.

Кэширование сессий и OCSP

ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;

Проверка клиентского сертификата добавляет нагрузку на CPU при каждом полном рукопожатии. Кэш на 10 МБ держит около 40 000 сессий, повторные подключения проходят без повторной верификации цепочки. OCSP stapling снижает задержку проверки серверного сертификата. При тысячах одновременных клиентов держите наготове второй воркер-процесс на ядро и следите за CPU steal на виртуалке.

Мониторинг сроков закрывает главный риск: просроченный CA обрушит все клиенты одновременно. Скрипт в cron с проверкой openssl x509 -checkend 2592000 предупредит за 30 дней до истечения. После усиления mTLS прогоните конфигурацию по чек-листу из аудита безопасности Nginx: там же разобраны защитные заголовки и ограничение доступа к служебным location.

Поделиться:
Сохранить гайд? В закладки браузера