SPOA (Stream Processing Offload Agent) для HAProxy: проверяет
клиентский сертификат TLS через OCSP и возвращает HAProxy результат в
виде переменной, которую можно использовать в ACL для reject/deny.
- HAProxy при mTLS-подключении завершает TLS handshake, и его
SPOE-движок (
filter spoe) по протоколу SPOP отправляет агенту сообщение с DER клиентского сертификата (ssl_c_der). - Агент парсит сертификат, находит его OCSP responder из AIA-расширения сертификата, находит сертификат издателя в локальном CA bundle, собирает и отправляет OCSP-запрос.
- Результат (
good/revoked/unknown/error) агент возвращает в SPOP-ответе как actionset-var; HAProxy кладёт его в транзакционную переменнуюocsp_status(+ocsp_reasonс деталями), читает её в ACL и решает, пропускать соединение или рвать его. - Результаты кэшируются в памяти по
(AuthorityKeyId, SerialNumber)на срок доNextUpdateиз ответа OCSP responder'а (в границахmin_cache_ttl/max_cache_ttlизconfig.yaml).
Проверку OCSP можно сделать и без внешнего процесса - написать
Lua-скрипт, встроенный в HAProxy (http-request lua.*, core.tcp()
для сетевого запроса к responder'у). У варианта с отдельным SPOE-агентом
перед этим есть конкретные преимущества:
- Неблокирующий I/O. Lua-скрипты HAProxy выполняются в том же event loop, что обслуживает соединения; синхронный сетевой запрос к OCSP responder'у из Lua блокирует этот worker на время round-trip. SPOE-фреймы асинхронны и пайплайнятся - HAProxy не подвисает, ожидая ответ агента.
- Готовый TLS/x509/OCSP-стек. Go stdlib (
crypto/x509) иgolang.org/x/crypto/ocspдают протестированный разбор сертификатов, ASN.1 и сборку/валидацию OCSP-запросов и ответов "из коробки". У Lua внутри HAProxy нет встроенного x509/ASN.1-парсера и OCSP-клиента. - Независимое масштабирование и деплой. Агента можно поднять как
backend с несколькими инстансами (
use-backend+ балансировка) и обновлять/перезапускать/катить новую версию отдельно от HAProxy, безreloadи разрыва существующих соединений. - Кэш переживает
reloadHAProxy. Агент - постоянный процесс, его in-memory TTL-кэш результатов не сбрасывается при каждом reload конфига HAProxy, в отличие от состояния, которое держал бы Lua внутри воркера.
Все настройки агента - в YAML-файле, программе передаётся только путь к нему:
./ocsp-agent -config config.yaml
-config по умолчанию ищет config.yaml в текущей директории.
Если файла по этому пути нет, агент создаёт его сам - копирует
initconfig.yaml (лежит рядом с бинарником, в корне
репозитория) в config.yaml и завершается с сообщением, что нужно
отредактировать хотя бы ca_bundle и запустить агента заново:
$ ./ocsp-agent -config config.yaml
2026/08/20 01:52:24 no config found at config.yaml, created it from
initconfig.yaml - edit it (at least ca_bundle) and start the agent again
initconfig.yaml при развёртывании должен лежать рядом с бинарником
(или в рабочей директории, из которой его запускают) - это просто
шаблон на диске, а не то, что встроено в сам бинарник.
Оба файла - config.yaml и initconfig.yaml - полностью
прокомментированы на русском прямо в самих файлах.
| Ключ | По умолчанию |
|---|---|
listen |
127.0.0.1:12345 |
ca_bundle |
(обязателен, значения по умолчанию нет) |
message_name |
check-client-cert |
cert_key |
cert |
ocsp_timeout |
4s |
min_cache_ttl |
5m |
max_cache_ttl |
1h |
error_cache_ttl |
30s |
listen - адрес и порт, на которых агент слушает входящие
SPOE-соединения от HAProxy. Обычно это localhost, так как HAProxy
обращается к агенту с той же машины (или из того же network
namespace/контейнера). Значение должно совпадать с адресом server в
backend'е, который в конфиге HAProxy указан как use-backend для
SPOE-агента (см. examples/haproxy.cfg).
ca_bundle (обязательный) - путь к PEM-файлу с сертификатами
издающих (issuing) CA - теми же промежуточными/корневыми
сертификатами, что указаны в ca-file на bind-строке HAProxy. Он
нужен агенту, чтобы построить корректный OCSP-запрос: структура CertID
требует hash имени и открытого ключа издателя, а не только сам
клиентский сертификат.
Файл может содержать несколько сертификатов CA подряд (обычная конкатенация PEM-блоков) - если клиентские сертификаты выпускаются разными независимыми CA, положите сюда сертификаты всех издателей. Агент при проверке конкретного сертификата сам подберёт из этого набора подходящего издателя.
message_name - имя SPOE-сообщения, которое агент ожидает
получить от HAProxy. Должно точно совпадать с именем в директиве
spoe-message в SPOE-конфиге (см. examples/ocsp.spoe.cfg, секция
spoe-message).
cert_key - имя ключа (KV) внутри SPOE-сообщения, по которому
агент достаёт DER-представление клиентского сертификата. Должно
совпадать с именем аргумента в args у spoe-message, например:
args cert=ssl_c_der.
ocsp_timeout - сколько агент ждёт ответа от OCSP responder'а на
один запрос, прежде чем считать попытку неудачной. Слишком маленькое
значение даёт ложные ошибки при чуть более медленной сети до
responder'а; слишком большое - долгую задержку обработки соединения в
HAProxy, если responder завис или недоступен (актуально при
fail-closed политике в ACL).
min_cache_ttl - TTL кэша результата, если ответ OCSP responder'а
не содержит поля NextUpdate (не все responder'ы его отдают). Без
этого поля агент не знает, на какой срок кэшировать ответ, и
использует это значение как запасное.
max_cache_ttl - верхняя граница жизни закэшированного
результата, даже если NextUpdate из ответа responder'а указывает на
более далёкий срок. Это защита от responder'а с некорректной или
подозрительно долгой настройкой NextUpdate: агент всё равно
перепроверит статус не позже, чем через max_cache_ttl.
error_cache_ttl - если запрос к OCSP responder'у не удался
(недоступен, таймаут, невалидный ответ), результат error кэшируется
на этот срок - чтобы при потоке новых соединений не долбить лежащий
responder повторными запросами на каждое из них.
Смотри examples/ocsp.spoe.cfg и examples/haproxy.cfg.
Ключевые моменты:
bind ... ssl ca-file client-ca.pem verify required- HAProxy сам проверяет цепочку доверия и подпись; агент добавляет поверх этого проверку отзыва через OCSP (то, что штатно HAProxy для клиентских сертификатов не делает).option var-prefix ocspв SPOE-конфиге - переменные агента оказываются доступны какvar(txn.ocsp.ocsp_status).- Fail-open vs fail-closed. Если агент недоступен, упал или не
ответил вовремя, HAProxy по умолчанию просто не получит переменную -
и ACL по
revokedеё не увидит, то есть трафик пройдёт (fail-open). Это стандартное поведение SPOE. Если нужна fail-closed-политика (блокировать при недоступности агента), явно проверяйтеvar(txn.ocsp.ocsp_status) -m found, как в примере конфига.
test/check-ocsp.sh - не тест самого агента, а быстрая проверка того,
что конкретный OCSP responder вообще доступен и корректно отвечает по
протоколу (независимо от того, отозван ли конкретный сертификат).
Нужен только openssl и curl.
test/check-ocsp.sh # URL по умолчанию
test/check-ocsp.sh -u http://tlss.lv.local:8080/ocsp # свой URL
test/check-ocsp.sh -c client.pem -i ca.pem # с реальным сертификатом/издателем
Без -c/-i скрипт сам генерирует одноразовую пару CA+сертификат -
responder её не узнает, но корректный ответ unauthorized/unknown
уже означает PASS (эндпоинт живой и говорит по OCSP). С -c/-i
скрипт печатает настоящий Cert Status (good/revoked/unknown).