Приёмники сертификатов: haproxy, nginx, OpenVPN¶
Alatyr выдаёт сертификат — принимает его чужая программа: обратный прокси, VPN-сервер, веб-сервер. Эта страница — про их настройку, и всё здесь измерено на живых приёмниках, а не выведено из документации.
Главное, что стоит знать заранее: у приёмника есть ровно одна правильная конфигурация и несколько похожих на правильную. Похожие пропускают чужой сертификат или не замечают отзыва, и узнаётся это не от них — они молчат.
Если вы пришли сюда настраивать mTLS с нуля
Сначала Настройка mTLS: там порядок шагов, и первый из
них — развести удостоверяющие центры целей. Проверка издателя, о которой
вся эта страница, разделяет цели только если центры разные. Пока
центр у user_mtls и wifi общий, издатель у обоих один, и ни одна
конфигурация ниже не поможет.
Две вещи, которые приёмник обязан проверять¶
Цепочку доверия — иначе войдёт любой сертификат любого удостоверяющего центра.
Издателя листового сертификата — иначе войдёт ЛЮБОЙ сертификат вашей же
PKI. Сертификат wifi (машинный, выдаётся молча) и сертификат user_mtls
(пользовательский, защищён подтверждением владельца) выписаны одним корнем и
несут один и тот же CN; если приёмник проверяет только цепочку, машинный
сертификат откроет доступ к приложению. Это не теория: в матрице ниже такая
конфигурация отвечает 200.
Плюс третья, про которую забывают: списки отзыва (CRL) нужны и на промежуточный удостоверяющий центр, и на корневой. С одним из двух отзыв не действует.
Проверка издателя и список отзыва — разные задачи, и подменять одну другой нельзя: первая разделяет цели, вторая закрывает отозванное. Кажется, что список отзыва заодно отсекает и машинный сертификат, — он отсекает его лишь потому, что для чужого промежуточного центра списка вы не дали. Стоит добавить и его список, и машинный сертификат снова пройдёт.
haproxy¶
Замерено на haproxy 3.0.27 (haproxy:3.0-alpine), сентябрь 2026.
frontend app_mtls
# ca-file — ПОЛНАЯ цепочка: промежуточный УЦ пользовательской mTLS + корень.
#
# С одним промежуточным haproxy рвёт рукопожатие ВСЕМ, включая законный
# сертификат: клиент видит ошибку TLS, до HTTP дело не доходит.
bind :443 ssl crt /etc/haproxy/tls/app.pem \
ca-file /etc/haproxy/tls/user-mtls-chain.pem verify required \
crl-file /etc/haproxy/tls/mtls-crl.pem
# ВОТ ЧТО РАЗДЕЛЯЕТ ЦЕЛИ. Без этих двух строк Wi-Fi-сертификат проходит
# проверку цепочки целиком и попадает в приложение.
acl issuer_ok ssl_c_i_dn(CN) -m str "Alatyr User mTLS Issuing CA"
http-request deny deny_status 403 unless issuer_ok
# `set-header` ЗАМЕЩАЕТ одноимённый заголовок клиента — подделать личность,
# прислав свой X-MTLS-User, нельзя. Именно set-header, не add-header:
# второй добавит значение рядом с клиентским, и приложение увидит оба.
http-request set-header X-MTLS-User %[ssl_c_s_dn(CN)]
default_backend app_upstream
nginx¶
Замерено на nginx 1.27.5, сентябрь 2026. Тот же блок с построчным разбором —
в Настройке mTLS.
# ПОЛНАЯ цепочка: промежуточный УЦ пользовательской mTLS И корень.
# С одним промежуточным nginx отвечает 400 ВСЕМ, включая законный сертификат.
ssl_client_certificate /etc/nginx/tls/user-mtls-chain.pem;
ssl_verify_client on;
ssl_verify_depth 2;
# Списки отзыва — тоже ОБА, склеенные в один файл.
ssl_crl /etc/nginx/tls/alatyr-crl.pem;
location / {
# ВОТ ЧТО РАЗДЕЛЯЕТ ЦЕЛИ. Формат RFC 2253: `CN=…`, без ведущей косой
# черты. Со старой формой `/CN=…` сравнение не совпадёт никогда.
if ($ssl_client_i_dn != "CN=Alatyr User mTLS Issuing CA") {
return 403;
}
proxy_set_header X-MTLS-User $ssl_client_s_dn_cn;
proxy_pass http://app_upstream;
}
Матрица замера: nginx 1.27.5 и haproxy 3.0.27¶
Одна PKI, одни и те же листья, живые приёмники. Пользовательский и Wi-Fi
сертификаты несут один и тот же CN=alice@example.com — как в продукте.
| Конфигурация приёмника | законный user_mtls |
wifi (чужая цель) |
отозванный user_mtls |
|---|---|---|---|
nginx, только промежуточный в ssl_client_certificate |
400 | 400 | 400 |
| nginx, промежуточный + корень | 200 | 200 ← обход | 200 |
| nginx, + проверка издателя | 200 | 403 | 200 ← отзыв не действует |
nginx, + ssl_crl только промежуточного |
400 | 400 | 400 |
nginx, + ssl_crl промежуточного и корня |
200 | 400 | 400 ✅ |
haproxy, только промежуточный в ca-file |
отказ TLS | отказ TLS | отказ TLS |
| haproxy, промежуточный + корень | 200 | 200 ← обход | 200 |
| haproxy, + ACL на издателя | 200 | 403 | 200 ← отзыв не действует |
haproxy, + crl-file только промежуточного |
отказ TLS | отказ TLS | отказ TLS |
haproxy, + crl-file промежуточного и корня |
200 | отказ TLS | отказ TLS ✅ |
Читается это так: рабочая конфигурация ровно одна на приёмник — последняя строка каждой половины. Остальные либо пропускают чужую цель, либо не действуют на отзыв, либо не работают вовсе.
Проверяйте обеими половинами: законным сертификатом И чужим. Конфигурация, проверенная только законным, неотличима от дырявой.
OpenVPN¶
Замерено на openvpn 2.6.14 (Debian), сентябрь 2026. Клиенты — openvpn с
ключом, который не покидает чип: на Linux через tpm2-pkcs11, на Windows через
cryptoapicert (ключ TPM в CNG), на macOS через интерфейс управления (ключ
Secure Enclave).
Сервер, минимум:
# ca — цепочка УЦ цели vpn: промежуточный И корень. Для встроенного УЦ:
# curl -s "$VAULT_ADDR/v1/pki_vpn/ca_chain" > /etc/openvpn/alatyr-vpn-chain.pem
ca /etc/openvpn/alatyr-vpn-chain.pem
cert /etc/openvpn/server.crt
key /etc/openvpn/server.key
remote-cert-tls client # требовать у клиента EKU clientAuth
crl-verify /etc/openvpn/vpn-crl.pem # без этой строки отзыв НЕ действует
# ВОТ ЧТО РАЗДЕЛЯЕТ ЦЕЛИ. Без проверки издателя сервер пускает сертификат
# ЛЮБОЙ цели того же корня — см. таблицу ниже.
script-security 2
tls-verify /etc/openvpn/alatyr-issuer-check.sh
/etc/openvpn/alatyr-issuer-check.sh (права 755):
#!/bin/sh
# OpenVPN зовёт скрипт на каждой глубине цепочки: $1 — глубина.
# На глубине 1 стоит издатель листа, его CN — в X509_1_CN.
[ "$1" != 1 ] && exit 0
[ "$X509_1_CN" = "Alatyr VPN Issuing CA" ] && exit 0
echo "issuer-check: отказ, издатель $X509_1_CN" >&2
exit 1
Почему «положить в ca только цепочку vpn» недостаточно¶
Кажется, что без промежуточного УЦ чужой цели сервер её сертификат не
проверит. Это не так: клиент вправе приложить свой промежуточный сам
(extra-certs), и OpenSSL построит цепочку через него до общего корня.
Замер 2026-09-23, сервер с ca = цепочка vpn, клиент — сертификат цели
user_mtls того же корня с приложенным промежуточным:
| Сервер | Итог |
|---|---|
без tls-verify |
Initialization Sequence Completed — туннель поднят сертификатом ЧУЖОЙ цели |
| с проверкой издателя выше | VERIFY SCRIPT ERROR: depth=1, CN=Alatyr User mTLS Issuing CA, туннеля нет |
законный сертификат vpn |
VERIFY SCRIPT OK: depth=1, CN=Alatyr VPN Issuing CA, туннель поднят |
Машинный сертификат wifi выдаётся без участия человека и тоже несёт
clientAuth — без проверки издателя он открыл бы VPN.
crl-verify у OpenVPN проверяет только лист: строки VERIFY WARNING:
depth=1 … unable to get certificate CRL в журнале — предупреждение, а не
отказ. Достаточно списка отзыва промежуточного УЦ vpn
($VAULT_ADDR/v1/pki_vpn/crl/pem).
Клиент с ключом в чипе — строки, которые печатает alatyr-agent vpn на машине
сотрудника (подробно — Настройка VPN):
# Linux
pkcs11-providers /usr/lib/x86_64-linux-gnu/pkcs11/libtpm2_pkcs11.so
pkcs11-id 'IBM/SW%20%20%20TPM/.../alatyr-agent/<CKA_ID>'
# Windows
cryptoapicert "THUMB:<отпечаток>"
# macOS
cert "<путь к сертификату>"
management "/var/run/alatyr-openvpn-<uid>.sock" unix
management-client-user <учётка сотрудника>
management-hold
management-external-key nopadding
Отзыв у OpenVPN действует без перезагрузки службы¶
Для прокси выше подмена файла CRL на диске сама по себе не делает ничего —
нужен reload. У OpenVPN не так, и это измерено:
| Шаг | Что сделано | Результат |
|---|---|---|
| 1 | подключение до отзыва | CONNECTED,SUCCESS, сервер записал CN владельца |
| 2 | сертификат отозван, новый CRL положен на диск, служба не перезапускалась | в журнале при следующем рукопожатии CRL: loaded 1 CRLs from file |
| 3 | подключение после отзыва | VERIFY ERROR: depth=0, error=certificate revoked, туннель не поднялся |
| 4 | выдан новый сертификат на ТОТ ЖЕ ключ, больше ничего не менялось | CONNECTED,SUCCESS снова |
Шаг 4 — доказательство чистоты замера: клиент, ключ и pkcs11-id те же,
изменился только сертификат, и исход перевернулся.
Вывод для оператора. Обновляйте файл CRL по расписанию — перезагрузка службы не нужна, существующие туннели не рвутся. Но верно и обратное: старый файл продолжает действовать молча. Просроченный CRL — это не «проверка выключена», а «проверка по устаревшим данным», и заметно это только по дате внутри файла.
Чеклист¶
- Удостоверяющие центры целей разведены на стороне Alatyr — шаг 1 настройки mTLS. Без этого пункты 2 и ниже не помогут.
- В
ca-file/ssl_client_certificate/ca— вся цепочка: промежуточный и корень. - Проверка издателя листа — отдельной строкой (у OpenVPN — скрипт
tls-verify). Без неё разделения целей нет. - CRL — на промежуточный и на корневой удостоверяющий центр.
- Обновление CRL по расписанию; для прокси — с перезагрузкой, для OpenVPN без неё.
- Заголовок личности — замещать (
set-header), а не добавлять. - Проверка приёмника — двумя сертификатами: законным и чужой цели.
Что дальше¶
- Настройка mTLS — порядок шагов целиком: развести центры, разрешить выдачу, одобрить заявку, научить приложение проверять сертификат.
- Отзыв доступа — готовый скрипт обновления списков отзыва по расписанию, с обеими точками монтирования.
- Нюансы и пределы — чего механизм не обещает, включая то,
почему
emailProtectionцелей не разделяет. - Диагностика — указатель по типовым отказам.