mTLS (mutual TLS) — это режим TLS, где сертификат предъявляют обе стороны: сервер доказывает свою подлинность клиенту, а клиент — серверу. Для API это удобная «админская» форма аутентификации: доступ привязан к закрытому ключу и цепочке доверия, а не к строке токена, которую можно случайно утянуть из логов или переменных окружения.
Ниже — практическая настройка mTLS для API на двух самых частых точках терминации: Nginx и HAProxy. Параллельно разберём выпуск тестовой PKI, отзыв сертификатов (CRL/OCSP) и то, как делать ротацию без простоя и сюрпризов.
Когда mTLS для API — действительно хорошая идея
mTLS чаще всего выбирают для сервис‑to‑сервис интеграций и «машинных» клиентов, где вы контролируете окружение выполнения и хранение ключей:
- внутренние API между микросервисами, особенно через балансировщик или API gateway;
- доступ партнёров к B2B API, когда у каждого партнёра должен быть свой идентификатор и возможность быстрого отзыва;
- админские/технические API (deploy hooks, internal tooling), куда не хочется пускать по длинноживущим токенам.
При этом mTLS не отменяет авторизацию уровня приложения. Типичный паттерн: mTLS отвечает за идентификацию и базовый допуск (кто ты), а приложение — за права (что тебе можно). Но в простых сценариях mTLS может быть и единственным барьером.
Самый частый источник проблем с mTLS — не конфиг прокси, а жизненный цикл сертификатов: выпуск, хранение закрытого ключа, отзыв, ротация и наблюдаемость. Если вы не готовы поддерживать эти процессы, токены могут оказаться проще.
PKI для mTLS: что именно нужно выпустить
Чтобы сервер мог проверить client certificate, ему нужен корневой (или промежуточный) удостоверяющий центр (CA), которому он доверяет, и клиентский сертификат, подписанный этим CA.
Минимальный набор:
- CA сертификат (или цепочка CA), которой будет доверять Nginx/HAProxy при проверке клиента;
- клиентский сертификат и соответствующий закрытый ключ на стороне клиента;
- понятные поля идентификации: например,
subjectAltName(SAN) с идентификатором клиента/сервиса или хотя быCN.
Практический совет: для API лучше заранее заложить «смысловую» идентичность в сертификат. Например:
- SAN DNS:
svc.billing.internalдля сервисов; - SAN URI:
spiffe://company/service/billing, если вы стандартизируете идентичности; - CN:
partner-acmeдля партнёрских клиентов (как минимум, чтобы удобно читать логи).
Быстрый выпуск тестовой PKI через OpenSSL (для стенда)
Ниже пример для стенда/лаборатории. В продакшене лучше иметь процессы и отдельную инфраструктуру/политику, но принцип тот же.
mkdir -p pki
cd pki
openssl genrsa -out ca.key 4096
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt -subj "/CN=Fastfox Demo Root CA"
openssl genrsa -out client.key 2048
openssl req -new -key client.key -out client.csr -subj "/CN=api-client-1"
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 365 -sha256
Для реального client certificate желательно задавать расширения, в частности Extended Key Usage: Client Authentication. Многие связки будут работать и без строгой проверки EKU, но если вы включите строгие политики, расширения должны быть корректными.
Если вы используете публично доверенные сертификаты на внешнем API, удобнее централизованно управлять продлением и цепочками — например, через SSL-сертификаты для фронтендов, а mTLS оставить для клиентской стороны вашей PKI.

Nginx: включаем проверку клиентского сертификата
В Nginx mTLS настраивается в HTTPS server блоке. Ключевые директивы:
ssl_client_certificate— CA/цепочка CA для проверки клиента;ssl_verify_client— режим проверки (обычноonилиoptional);ssl_verify_depth— глубина цепочки (полезно при промежуточных CA).
Жёсткий режим: mTLS обязателен для всего vhost
server {
listen 443 ssl;
server_name api.example.internal;
ssl_certificate /etc/nginx/tls/server.fullchain.pem;
ssl_certificate_key /etc/nginx/tls/server.key;
ssl_client_certificate /etc/nginx/mtls/clients-ca.crt;
ssl_verify_client on;
ssl_verify_depth 2;
location / {
proxy_pass http://app_upstream;
proxy_set_header X-Client-Verify $ssl_client_verify;
proxy_set_header X-Client-DN $ssl_client_s_dn;
proxy_set_header X-Client-Fingerprint $ssl_client_fingerprint;
}
}
Полезные переменные Nginx:
$ssl_client_verify:SUCCESS,FAILED,NONE;$ssl_client_s_dn: subject DN клиента;$ssl_client_fingerprint: отпечаток сертификата (удобно как стабильный идентификатор для логов);$ssl_client_escaped_cert: сертификат строкой (часто избыточно и тяжело для логов и заголовков).
Важно: если вы проксируете идентичность дальше в приложение, не принимайте заголовки X-Client-* «на веру», иначе клиент может прислать их сам. Практика: очистить входящие заголовки и выставлять свои только на mTLS-входе.
proxy_set_header X-Client-Verify "";
proxy_set_header X-Client-DN "";
proxy_set_header X-Client-Fingerprint "";
proxy_set_header X-Client-Verify $ssl_client_verify;
proxy_set_header X-Client-DN $ssl_client_s_dn;
proxy_set_header X-Client-Fingerprint $ssl_client_fingerprint;
Мягкий режим: mTLS только для части API
Иногда нужно оставить публичный endpoint (например, /health для внешнего мониторинга) и закрыть только часть маршрутов (/admin, /partner, /private).
Тонкость: ssl_verify_client применяется на уровне server, но можно включить optional, а затем в нужных location проверять результат.
server {
listen 443 ssl;
server_name api.example.internal;
ssl_certificate /etc/nginx/tls/server.fullchain.pem;
ssl_certificate_key /etc/nginx/tls/server.key;
ssl_client_certificate /etc/nginx/mtls/clients-ca.crt;
ssl_verify_client optional;
location = /health {
return 200;
}
location /private/ {
if ($ssl_client_verify != SUCCESS) { return 403; }
proxy_pass http://app_upstream;
}
location / {
proxy_pass http://app_upstream;
}
}
Конструкция if в Nginx имеет нюансы, но для простого return обычно безопасна. Если хотите «чище» — делайте через map и переменную с кодом ответа.
Если нужно сравнить подходы Nginx и Apache по клиентским сертификатам, пригодится отдельный разбор: mTLS и проверка client certificate в Apache.
HAProxy: mTLS на фронтенде
В HAProxy проверка клиента задаётся SSL‑параметрами в bind. Ключевые элементы:
verify required— требовать клиентский сертификат;ca-file— доверенная цепочка CA;crl-file— CRL, если используете отзыв через CRL;- ACL по данным сертификата: DN, fingerprint, issuer.
Пример фронтенда с обязательным mTLS
frontend fe_api_tls
bind :443 ssl crt /etc/haproxy/tls/server.pem ca-file /etc/haproxy/mtls/clients-ca.crt verify required
mode http
option httplog
http-request set-header X-Client-Verify %[ssl_c_verify]
http-request set-header X-Client-DN %[ssl_c_s_dn]
http-request set-header X-Client-Fingerprint %[ssl_c_sha1]
default_backend be_api
Пара примечаний по выборке:
%[ssl_c_verify]— результат проверки (в популярных конфигурациях 0 означает успех);%[ssl_c_s_dn]— subject DN;%[ssl_c_sha1]— отпечаток SHA1: как идентификатор лучше SHA256, но в инфраструктуре часто используют то, что проще собрать в ACL/логи.
mTLS только для части путей
Частая схема: один host/порт общий, но для части URL требуем клиентский сертификат через ACL. Для этого на bind включают verify optional, а затем запрещают доступ, если сертификата нет или он невалиден.
frontend fe_api_tls
bind :443 ssl crt /etc/haproxy/tls/server.pem ca-file /etc/haproxy/mtls/clients-ca.crt verify optional
mode http
acl is_private path_beg /private/
acl mtls_ok ssl_c_verify 0
http-request deny deny_status 403 if is_private !mtls_ok
default_backend be_api
Так публичные маршруты будут работать без клиентского сертификата, а приватные — только при успешной проверке.
Когда mTLS завершается на отдельном балансировщике, нередко удобнее держать такую точку терминации на выделенной машине и масштабировать независимо от приложения. Для этого подойдёт VDS с предсказуемой сетью и возможностью быстро добавлять фронтенды.
CRL и OCSP: как отзывать клиентские сертификаты
Сильная сторона mTLS — адресный отзыв доступа: можно заблокировать конкретного клиента, не трогая остальных. Вопрос — как точка терминации узнаёт, что сертификат отозван.
CRL (Certificate Revocation List)
CRL — это файл со списком серийных номеров отозванных сертификатов. Плюсы: простота и автономность (не нужен сетевой доступ к OCSP). Минусы: файл нужно регулярно обновлять и доставлять на все узлы, а при большом количестве отзывов CRL растёт.
Поддержка на практике:
- Nginx: отзыв через CRL подключают директивой
ssl_crl(зависит от сборки и версии); - HAProxy:
crl-fileнаbind.
Пример для HAProxy:
bind :443 ssl crt /etc/haproxy/tls/server.pem ca-file /etc/haproxy/mtls/clients-ca.crt crl-file /etc/haproxy/mtls/clients.crl verify required
Операционный момент: держите CRL рядом с пайплайном отзыва. Как только вы добавили сертификат в список отзыва, убедитесь, что новый CRL доставлен на все точки терминации и сервис перечитал конфигурацию (reload).
OCSP для клиентских сертификатов
OCSP чаще обсуждают для серверных сертификатов и stapling. Теоретически механизм применим и к клиентским, но на практике в mTLS‑сценариях часто выбирают CRL или короткий TTL клиентских сертификатов, потому что:
- проверка OCSP на фронтенде может требовать сетевого доступа и добавляет вариативность задержек;
- нужно продумать отказоустойчивость (что делать при недоступности OCSP: fail-open или fail-closed);
- не все окружения одинаково предсказуемо ведут себя с OCSP именно для клиентской стороны.
Если вы строите PKI «по-взрослому», закладывайте наблюдаемость: логи и метрики по статусам проверок, чтобы отличать отзыв сертификата от инфраструктурной деградации.

Идентификация клиента: CN, SAN или fingerprint
В mTLS важно заранее решить, что будет «логином» клиента. Варианты:
- Fingerprint — однозначно и удобно для allowlist/denylist, но при перевыпуске сертификата меняется, и правила надо обновлять;
- CN — просто, но может быть не уникальным и считается устаревающим местом для идентичности;
- SAN — лучший выбор для идентичности: можно хранить тип (DNS/URI) и структуру.
Практичный подход для API: хранить в SAN «человеческий» идентификатор клиента/сервиса, а fingerprint использовать как дополнительный атрибут для расследований и быстрого блокирования.
Ротация сертификатов без простоя
Ротация — самая частая эксплуатационная задача. Чтобы она не превращалась в аврал:
- делайте клиентские сертификаты короткими (например, 30–90 дней) и автоматизируйте выпуск;
- поддерживайте одновременную валидность старого и нового сертификата клиента на время переката;
- если меняете CA, добавляйте новый CA в trust store заранее и держите оба до завершения миграции;
- проверяйте, что reload конфигурации не рвёт соединения (у Nginx и HAProxy обычно всё нормально при корректном reload).
Типовая схема смены CA:
- Добавить новый CA в
ssl_client_certificate/ca-file(цепочкой), оставить старый. - Выпустить клиентам новые сертификаты от нового CA и развернуть.
- Проверить, что все клиенты перешли (по логам/метрикам и списку активных fingerprint).
- Убрать старый CA, обновить CRL/политику.
Проверка и диагностика: как быстро понять, что не так
Для первичной проверки удобно использовать openssl s_client: можно явно указать клиентский сертификат и ключ и увидеть, прошла ли взаимная проверка.
openssl s_client -connect api.example.internal:443 -servername api.example.internal -cert client.crt -key client.key -CAfile ca.crt
Что смотреть в выводе:
- успешность TLS‑рукопожатия;
- ошибки валидации цепочки (не тот CA, неполная цепочка, неверный depth);
- совпадает ли SNI (
-servername) с сертификатом сервера.
Для API поверх HTTP дополнительно проверьте запрос через curl с клиентским сертификатом:
curl -v --cert client.crt --key client.key --cacert ca.crt https://api.example.internal/private/ping
Частые ошибки в mTLS и как их избегать
Сервер «не видит» промежуточный CA
Симптом: клиентский сертификат вроде подписан вашим CA, но прокси не принимает. Частая причина — в trust store лежит не та цепочка (доверили только root, а у вас intermediate, или наоборот). Решение: в ssl_client_certificate/ca-file держать правильную цепочку CA, которая именно валидирует клиентские сертификаты.
Слишком широкое доверие
Если в trust store положить «корпоративный» CA, которым подписывают вообще всё, вы рискуете тем, что любой сертификат из этого контура сможет пройти как клиентский. Для API лучше заводить отдельный CA или отдельный intermediate под клиентов API.
Нет политики отзыва
mTLS без отзыва — это почти как вечные токены. Минимум: короткий срок жизни client cert + процесс мгновенной блокировки (CRL или denylist по fingerprint). И обязательно тестируйте отзыв, а не только выпуск.
Потеря идентичности на уровне приложения
Если у вас несколько прокси, можно потерять контекст «кто клиент». Либо завершайте mTLS на самом приложении, либо стандартизируйте передачу идентичности (DN/SAN/fingerprint) и защищайте эти заголовки от подмены на границе.
Итоговый чек-лист для mTLS в API
- Определили модель идентичности (SAN/CN/fingerprint) и формат логирования.
- Выпустили отдельный CA (или intermediate) под API‑клиентов.
- Настроили Nginx (
ssl_client_certificate+ssl_verify_client) или HAProxy (verify required/verify optional). - Продумали отзыв: CRL и доставка на точки, либо альтернативная стратегия (короткий TTL + denylist/allowlist).
- Сделали ротацию: параллельная валидность, миграция CA без простоя.
- Добавили диагностику:
openssl s_client, curl, логи по результатам$ssl_client_verify/ssl_c_verify.
Если вы один раз выстроите PKI‑процесс и дисциплину ротации, mTLS становится предсказуемым способом защищать API — с понятным аудитом и быстрым отзывом доступа без «перевыпуска всех токенов».


