Взаимная 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_client | on / optional / optional_no_ca / off | Режим проверки клиентского сертификата |
| ssl_verify_depth | 1-3 | Максимальная длина цепочки промежуточных CA |
| ssl_crl | путь к crl.pem | Список отозванных сертификатов, читается при старте |
| ssl_session_cache | shared: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_dn | Subject DN клиента | Авторизация по CN, логирование доступа |
| $ssl_client_verify | SUCCESS, FAILED или NONE | Отсечение неаутентифицированных запросов в режиме optional |
| $ssl_client_serial | Серийный номер сертификата | Точный отзыв конкретного экземпляра |
| $ssl_client_fingerprint | SHA1-отпечаток | Привязка ключа внутри приложения |
Проверка 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, чаще из-за несовпадения протоколов, кривого ключа или обрыва балансировщиком.
Чек-лист диагностики
- Проверить значение ssl_verify_client: режим on требует сертификат от каждого запроса.
- Проверить путь в ssl_client_certificate и права на чтение файла у пользователя, от которого работает Nginx.
- Проверить срок, SAN и расширение clientAuth у клиентского сертификата.
- Проверить цепочку командой openssl verify -CAfile ca.crt client.crt.
- Сопоставить код 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.