Полное руководство по установке, настройке и эксплуатации HA-WAF.
HA-WAF — это единый исполняемый файл (Go binary), который объединяет:
HA-WAF управляет HAProxy как дочерним процессом: генерирует конфиг, запускает, перезагружает (бесшовно, через SIGUSR2) и взаимодействует через admin socket. WAF работает через SPOE-протокол на локальном TCP-порту.
| Категория | Возможности |
|---|---|
| Проксирование | HTTP/HTTPS (L7), TCP с SNI-маршрутизацией (L4), балансировка нагрузки (roundrobin, leastconn, random, source, first) |
| WAF | Coraza + OWASP CRS v4, paranoia levels 1–4, режимы detection/prevention, per-site изоляция |
| TLS | Мультиаккаунтный ACME (HTTP-01, DNS-01 для 8 провайдеров), загрузка PEM, массовый импорт архивов |
| GeoIP | Блокировка по стране происхождения трафика, per-site настройка |
| IP-фильтрация | Whitelist (включая bypass WAF) и blacklist по CIDR-диапазонам |
| Rate Limiting | Ограничение по conn_rate, bytes_in_rate, http_req_rate; опциональная привязка к URL-пути |
| Path Rules | nginx-аналог location blocks: проксирование, статический ответ, перенаправление; условия, WebSocket, gRPC, модификация заголовков |
| Observability | OpenTelemetry OTLP (traces, metrics, logs), встроенный Prometheus scrape |
| HA | Multi-replica через PostgreSQL LISTEN/NOTIFY |
| Конфиг | Ревизии, diff, rollback, экспорт/импорт JSON |
Стандартная редакция (HA-WAF):
┌──────────────────────────────────────────┐
│ ha-waf process │
Клиент ─── :80/443 ──►│ │
│ HAProxy 3.2 (child) ◄──SPOE──► Coraza │
Admin ─── :8080 ───►│ REST API + Web UI │
│ SQLite / PostgreSQL │
└──────────────────────────────────────────┘
При каждом изменении конфигурации HA-WAF:
git clone https://github.com/ha-waf/ha-waf
cd ha-waf
docker compose up -d
Стек по умолчанию запускает:
Web UI доступен по адресу: http://localhost:8080
Учётные данные по умолчанию: admin / admin
Первое, что нужно сделать: сменить пароль администратора в разделе Settings → Users.
Смонтируйте свой config.yaml:
# docker-compose.override.yml
services:
ha-waf:
volumes:
- ./my-config.yaml:/etc/ha-waf/config.yaml:ro
Чарт лежит в репозитории по пути deploy/ha-waf/ (не charts/ha-waf) и
публикуется в OCI-registry (см. §73):
helm install ha-waf oci://ghcr.io/ha-waf/charts --version 0.4.1 \
--set image.tag=v0.3.0 \
--set config.database.driver=postgres \
--set config.database.dsn="host=pg-primary ..." \
--set ingress.enabled=true
Или локально из исходников: helm install ha-waf deploy/ha-waf/.
Чарт создаёт:
Deployment с readinessProbe на /readyzService (ha-waf-api — ClusterIP для управления, ha-waf-proxy —
LoadBalancer на портах 80/443, опционально доп. порты через
service.proxy.extraPorts)ServiceAccount (automountServiceAccountToken включён только при
k8sNetworkBan.enabled)ClusterRole/ClusterRoleBinding — только при k8sNetworkBan.enabled
(RBAC на ciliumclusterwidenetworkpolicies, см. Часть XIV: k8sban)Ingress (классический, ingress.enabled) и/или HTTPRoute (Gateway
API, gateway.enabled) для UI/APIPodDisruptionBudget, HorizontalPodAutoscalerPersistentVolumeClaim для SQLite (или используйте PostgreSQL)ConfigMap для config.yamlПри горизонтальном масштабировании используйте PostgreSQL с
dsn_readдля read replica. SQLite не поддерживает multi-replica.
server: { ... } # REST API сервер
database: { ... } # SQLite или PostgreSQL
haproxy: { ... } # HAProxy параметры
spoa: { ... } # Coraza SPOE адрес
auth: { ... } # JWT настройки
telemetry: { ... } # Prometheus + OTLP
Флаг при запуске: -config /path/to/config.yaml (по умолчанию /etc/ha-waf/config.yaml).
server:
addr: ":8080" # Адрес REST API и Web UI
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
addr |
string | :8080 |
Адрес прослушивания API-сервера |
database:
driver: sqlite
dsn: /var/lib/ha-waf/ha-waf.db
database:
driver: sqlite
dsn: /var/lib/ha-waf/ha-waf.db
database:
driver: postgres
dsn: "host=localhost port=5432 user=hawaf password=secret dbname=hawaf sslmode=disable"
database:
driver: postgres
dsn: "host=pg-primary port=5432 user=hawaf password=secret dbname=hawaf sslmode=disable"
dsn_read: "host=pg-replica port=5433 user=hawaf password=secret dbname=hawaf sslmode=disable"
Запросы на запись всегда идут на dsn (primary). Read-only запросы (GET) — на dsn_read.
| Параметр | Описание |
|---|---|
driver |
sqlite или postgres |
dsn |
Путь к файлу (SQLite) или строка подключения (PostgreSQL) |
dsn_read |
Read replica DSN (только PostgreSQL) |
haproxy:
binary: /usr/local/sbin/haproxy
config_dir: /etc/ha-waf/haproxy
config_file: /etc/ha-waf/haproxy/haproxy.cfg
pid_file: /var/run/ha-waf/haproxy.pid
socket_path: /var/run/ha-waf/haproxy-admin.sock
maxconn: 50000
ulimit_n: 200000
ssl_options: "no-sslv3 no-tlsv10 no-tlsv11"
http_port: 80
https_port: 443
ssl_cert_dir: /etc/ha-waf/certs/
error_dir: /etc/ha-waf/errors
geoip_map_file: /etc/ha-waf/geoip.txt
metrics_bind: ":8405"
metrics_acls:
- "127.0.0.1"
- "192.168.0.0/16"
internal_rate_table_port: 19999
| Параметр | По умолчанию | Описание |
|---|---|---|
binary |
/usr/local/sbin/haproxy |
Путь к бинарнику HAProxy |
config_dir |
/etc/ha-waf/haproxy |
Директория для генерации конфигов |
config_file |
...haproxy.cfg |
Путь к генерируемому конфиг-файлу |
pid_file |
/var/run/ha-waf/haproxy.pid |
PID-файл HAProxy |
socket_path |
/var/run/ha-waf/haproxy-admin.sock |
Admin socket для live-команд |
maxconn |
50000 |
Глобальный лимит одновременных соединений |
ulimit_n |
200000 |
Лимит открытых файловых дескрипторов |
ssl_options |
no-sslv3 no-tlsv10 no-tlsv11 |
Отключённые SSL-протоколы |
http_port |
80 |
Порт HTTP frontend |
https_port |
443 |
Порт HTTPS frontend |
ssl_cert_dir |
/etc/ha-waf/certs/ |
Директория PEM-сертификатов |
error_dir |
/etc/ha-waf/errors |
Директория HTML-страниц ошибок |
geoip_map_file |
/etc/ha-waf/geoip.txt |
Путь к GeoIP-карте (формат CIDR→код страны) |
metrics_bind |
:8405 |
Адрес HAProxy Prometheus endpoint |
metrics_acls |
["127.0.0.1"] |
CIDR-список источников, разрешённых на /metrics |
internal_rate_table_port |
19999 |
Внутренний порт stick-table для rate limiting |
spoa:
addr: "127.0.0.1:9000"
| Параметр | По умолчанию | Описание |
|---|---|---|
addr |
127.0.0.1:9000 |
TCP-адрес SPOE-сервера Coraza |
Не выставляйте SPOA наружу. Это внутренний протокол между HAProxy и Coraza.
auth:
jwt_secret: "your-very-secret-key-here"
token_ttl: "24h"
| Параметр | По умолчанию | Описание |
|---|---|---|
jwt_secret |
(случайный при старте) | Секрет подписи JWT. Установите явно в продакшне — иначе сессии сбрасываются при рестарте |
token_ttl |
24h |
Время жизни JWT-токена (формат Go duration: 1h, 24h, 168h) |
В multi-replica режиме обязательно задайте одинаковый
jwt_secretна всех узлах.
telemetry:
prometheus_addr: ":9091"
otlp:
endpoint: "otel-collector:4317"
insecure: true
logs:
enabled: true
metrics:
enabled: true
traces:
enabled: true
| Параметр | Описание |
|---|---|
prometheus_addr |
Адрес Prometheus scrape endpoint HA-WAF (пусто = отключено) |
otlp.endpoint |
gRPC адрес OTLP-коллектора |
otlp.insecure |
true = без TLS (для локального коллектора) |
otlp.logs.enabled |
Отправка структурированных логов через OTLP |
otlp.metrics.enabled |
Отправка метрик через OTLP |
otlp.traces.enabled |
Отправка трассировок через OTLP |
Web UI: Раздел Sites → кнопка Add Site.
API:
POST /api/v1/sites
Content-Type: application/json
{
"name": "My App",
"enabled": true,
"mode": "http",
"domains": ["app.example.com"],
"domain_suffixes": [".app.example.com"],
"http_enabled": true,
"https_enabled": true,
"redirect_http_to_https": true,
"acme_enabled": true,
"lb_algorithm": "roundrobin"
}
| Поле | Тип | Описание |
|---|---|---|
name |
string | Отображаемое имя сайта |
enabled |
bool | Включить/отключить сайт (без удаления) |
mode |
http | tcp |
Режим работы: L7 HTTP или L4 TCP с SNI |
domains |
[]string | Точные имена хостов (Host: app.example.com) |
domain_suffixes |
[]string | Суффиксы хостов (.example.com → любой поддомен) |
http_enabled |
bool | Принимать HTTP-запросы на порту 80 |
https_enabled |
bool | Принимать HTTPS-запросы на порту 443 |
redirect_http_to_https |
bool | 301-редирект HTTP → HTTPS (кроме ACME challenge) |
acme_enabled |
bool | Разрешить автоматический выпуск TLS-сертификата |
acme_cert_name |
string | Имя выпущенного ACME-сертификата (readonly) |
ssl_cert_dir |
string | Override глобальной директории сертификатов |
lb_algorithm |
string | Алгоритм балансировки (см. раздел 17) |
tcp_port |
int | Порт для TCP-режима |
Каждый сайт получает ACL в HAProxy. Запрос направляется к сайту, если:
Host header точно совпадает с одним из domains, илиHost header заканчивается на один из domain_suffixes, илиDomainList (список доменов), илиdomains, domain_suffixes и domain_lists — всегда (catch-all)TCP-сайты работают в режиме L4 pass-through: HAProxy не расшифровывает TLS, а перенаправляет трафик по SNI-имени хоста.
POST /api/v1/sites
{
"name": "NetBird",
"enabled": true,
"mode": "tcp",
"tcp_port": 443,
"domains": ["vpn.example.com"],
"domain_suffixes": [],
"lb_algorithm": "roundrobin"
}
Как работает SNI-маршрутизация:
Если на одном TCP-порту несколько сайтов с разными domains/domain_suffixes, HAProxy:
tcp-request inspect-delay 5s (ждёт TLS ClientHello)req.ssl_sniЕсли только один сайт на порту — SNI-инспекция не включается (нет overhead).
Сайт без domains и domain_suffixes становится default_backend для данного порта.
Ограничения TCP-режима: WAF, GeoIP, IP-списки, Path Rules, Rate Limits не работают в TCP-режиме — HAProxy не видит HTTP-содержимое.
К каждому сайту можно добавить несколько бэкенд-серверов.
API:
POST /api/v1/sites/{siteId}/backends
{
"name": "web-1",
"address": "10.0.0.10",
"port": 8080,
"weight": 10,
"enabled": true,
"ssl_enabled": false,
"ssl_verify_none": false,
"ssl_sni_from_host": false,
"send_proxy": 0
}
| Поле | Тип | Описание |
|---|---|---|
name |
string | Имя сервера (уникальное в рамках сайта) |
address |
string | IP или hostname бэкенда |
port |
int | TCP-порт |
weight |
int | Вес при балансировке (1–256, по умолчанию 10) |
enabled |
bool | Включить/отключить сервер |
ssl_enabled |
bool | Реэнкрипт: соединение к бэкенду по HTTPS/TLS |
ssl_verify_none |
bool | Не проверять TLS-сертификат бэкенда |
ssl_sni_from_host |
bool | Передавать Host header в TLS SNI к бэкенду |
send_proxy |
int | PROXY protocol: 0 — нет, 1 — v1, 2 — v2 |
health_check_enabled |
bool | Включить HTTP health check |
health_check_uri |
string | URI для health check (по умолчанию /) |
health_check_interval |
int | Интервал проверки в секундах |
health_check_fall |
int | Число неудачных проверок для перевода в down |
health_check_rise |
int | Число успешных проверок для возврата в up |
| Значение | Описание |
|---|---|
roundrobin |
По кругу (с учётом веса) |
leastconn |
Наименьшее число активных соединений |
random |
Случайный выбор |
source |
По IP клиента (sticky по источнику) |
first |
Всегда первый доступный сервер |
Domain Lists позволяют загружать списки доменов из внешних URL и маршрутизировать трафик на специальный бэкенд.
Применение:
POST /api/v1/sites/{siteId}/domainlists
{
"name": "Client Domains",
"enabled": true,
"source_url": "https://config.example.com/domains.txt",
"fetch_interval": 300,
"auth_user": "user",
"auth_password": "secret",
"backend_id": "uuid-of-backend"
}
Один домен на строку:
client1.example.com
client2.example.com
# комментарии игнорируются
| Поле | Описание |
|---|---|
source_url |
URL для загрузки списка (HTTP/HTTPS) |
fetch_interval |
Интервал обновления в секундах |
auth_user / auth_password |
Basic Auth для загрузки |
backend_id |
UUID бэкенда для этой группы доменов (если не задан — используется основной бэкенд сайта) |
Каждый сайт имеет одну политику WAF (1:1). Политика управляет поведением Coraza + OWASP CRS.
API:
GET /api/v1/sites/{siteId}/waf
PUT /api/v1/sites/{siteId}/waf
{
"enabled": true,
"mode": "prevention",
"paranoia_level": 1,
"anomaly_threshold": 5,
"response_body_check": false,
"custom_directives": ""
}
| Режим | Поведение | Когда использовать |
|---|---|---|
detection |
Логирует нарушения, не блокирует | При начальном развёртывании, для изучения ложных срабатываний |
prevention |
Блокирует запросы при нарушениях | Продакшн-защита |
Рекомендуемый workflow: начните с
detection, изучите WAF-события в UI, добавьте исключения для ложных срабатываний, затем переключитесь наprevention.
Paranoia Level (PL) определяет, сколько правил CRS активировано. Более высокий уровень = лучшая защита, но больше ложных срабатываний.
| PL | Правила | Описание |
|---|---|---|
| 1 | Базовые | Минимум ложных срабатываний, рекомендуется для старта |
| 2 | + Средние | Умеренная защита |
| 3 | + Строгие | Высокая защита, требует тонкой настройки исключений |
| 4 | + Параноидальные | Максимальная защита, высокая вероятность ложных срабатываний |
Исключения позволяют отключить конкретные CRS-правила или сузить их область применения.
API:
POST /api/v1/sites/{siteId}/exclusions
{
"rule_id": 942100,
"type": "disable",
"comment": "SQL injection false positive in search form"
}
| Тип | Описание | Дополнительные поля |
|---|---|---|
disable |
Полностью отключить правило для сайта | — |
exclude_arg |
Не применять правило к конкретному аргументу запроса | target (имя параметра) |
exclude_request_header |
Не применять правило к конкретному заголовку | target (имя заголовка) |
exclude_url |
Отключить правило для конкретного URL-пути | url_pattern (начало пути) |
Пример: отключить проверку поля q для правила 942100:
{
"rule_id": 942100,
"type": "exclude_arg",
"target": "q"
}
Пример: отключить правило только для /api/search:
{
"rule_id": 942100,
"type": "exclude_url",
"url_pattern": "/api/search"
}
Найти номер правила можно в разделе WAF Events — там отображается Rule ID сработавшего правила.
Custom Rules позволяют писать произвольные директивы Coraza/ModSecurity.
Глобальные правила (site_id = "") применяются ко всем сайтам.
Per-site правила применяются только к конкретному сайту.
POST /api/v1/sites/{siteId}/rules
{
"name": "Block suspicious UA",
"enabled": true,
"priority": 10100,
"directives": "SecRule REQUEST_HEADERS:User-Agent \"@contains evil-bot\" \"id:10100,phase:1,deny,status:403,msg:'Blocked UA'\""
}
Глобальные правила:
POST /api/v1/rules
{
"name": "Global: block tor exit nodes",
"priority": 9000,
"directives": "..."
}
| Поле | Описание |
|---|---|
name |
Имя правила (отображается в UI) |
enabled |
Включено/отключено |
priority |
Порядок применения (меньше = раньше). Рекомендуемый диапазон: 10000–19999 |
directives |
Директивы Coraza (SecRule, SecAction, SecRuleRemoveById и т.д.) |
1. SecRuleRemoveById (исключения disable)
2. SecRuleUpdateTargetById (исключения exclude_arg/header)
3. URL-scoped исключения (exclude_url)
4. Глобальные custom rules (sorted by priority)
5. Per-site custom rules (sorted by priority)
6. WAFPolicy.custom_directives (inline поле)
Раздел UI: WAF Events (доступен глобально или через вкладку сайта).
Каждое срабатывание WAF записывает событие:
| Поле | Описание |
|---|---|
site_id |
UUID сайта |
rule_id |
Номер правила CRS |
message |
Описание правила |
severity |
Критичность (CRITICAL, ERROR, WARNING, NOTICE) |
action |
deny или detect |
src_ip |
IP источника |
method |
HTTP метод |
uri |
Запрошенный URI |
timestamp |
Время события |
Используйте Rule ID из событий для создания точечных исключений.
GeoIP-политика позволяет разрешить трафик только из определённых стран.
API:
GET /api/v1/sites/{siteId}/geoip
PUT /api/v1/sites/{siteId}/geoip
{
"enabled": true,
"map_file": "/etc/ha-waf/geoip.txt",
"allowed_countries": ["RU", "BY", "KZ"],
"deny_status": 403,
"add_country_header": true
}
| Поле | Описание |
|---|---|
enabled |
Включить GeoIP-фильтрацию |
map_file |
Путь к GeoIP-карте (override глобальной) |
allowed_countries |
Коды стран ISO 3166-1 alpha-2, трафик из которых разрешён |
deny_status |
HTTP-код для заблокированных (403 или 451) |
add_country_header |
Добавлять заголовок X-Country: RU к разрешённым запросам |
Логика: Если IP найден в карте и его страна НЕ в
allowed_countries— блокировать. Если IP не найден в карте — разрешить (не блокировать неизвестных).
Файл карты: один CIDR на строку, разделённый пробелом от кода страны:
# GeoIP map — формат: CIDR ISO-3166-1
1.0.0.0/24 AU
1.0.1.0/24 CN
5.8.18.0/23 RU
77.88.0.0/18 RU
HA-WAF не скачивает GeoIP автоматически — вы должны предоставить карту самостоятельно. Рекомендуемые источники:
При обновлении файла на диске достаточно нажать Reload в UI или вызвать POST /api/v1/reload.
Whitelist разрешает трафик с указанных CIDR минуя все проверки.
POST /api/v1/sites/{siteId}/iplists
{
"name": "Internal Networks",
"type": "whitelist",
"enabled": true,
"cidrs": ["10.0.0.0/8", "192.168.0.0/16", "172.16.0.0/12"],
"bypass_waf": true
}
| Поле | Описание |
|---|---|
type |
whitelist |
cidrs |
Список CIDR-диапазонов |
bypass_waf |
true = WAF не применяется к этим IP (allow без WAF check) |
Если
bypass_waf = false, трафик разрешён, но WAF всё равно проверяет запросы.
Blacklist блокирует трафик с указанных CIDR с кодом 403.
POST /api/v1/sites/{siteId}/iplists
{
"name": "Blocked IPs",
"type": "blacklist",
"enabled": true,
"cidrs": ["1.2.3.4/32", "5.6.7.0/24"]
}
Порядок проверок в HAProxy:
1. Blacklist: IP в чёрном списке? → 403
2. GeoIP: страна запрещена? → 403
3. Whitelist + bypass_waf: IP в белом списке? → разрешить (пропустить WAF)
4. WAF проверка
5. Whitelist без bypass_waf: IP в белом списке? → разрешить (после WAF)
Rate Limiting работает через shared stick-table в HAProxy. Ограничения применяются по IP клиента.
POST /api/v1/sites/{siteId}/ratelimits
{
"name": "Global rate limit",
"enabled": true,
"http_req_rate": 100,
"conn_rate": 50,
"bytes_in_rate": 1048576,
"deny_status": 429
}
| Поле | Единица | Описание |
|---|---|---|
http_req_rate |
запросов/15сек | Максимум HTTP-запросов за 15 секунд с одного IP |
conn_rate |
соединений/5сек | Максимум TCP-соединений за 5 секунд |
bytes_in_rate |
байт/15сек | Максимум входящих байт за 15 секунд |
deny_status |
HTTP код | Код ответа при превышении (рекомендуется 429) |
Можно задать несколько правил — они все применяются (логика OR: любое превышение → блокировка).
Rate Limit можно привязать только к определённому пути:
POST /api/v1/sites/{siteId}/ratelimits
{
"name": "API rate limit",
"http_req_rate": 30,
"path_prefixes": ["/api/", "/graphql"],
"deny_status": 429
}
Или по регулярному выражению:
{
"path_regex": "^/api/v[0-9]+/",
"http_req_rate": 20
}
path_prefixesиpath_regexвзаимоисключающие. Если оба пусты — правило применяется ко всем запросам сайта.
Path Rules — аналог location блоков в nginx. Позволяют настроить специальное поведение для конкретных URL-путей L7 сайта:
Вкладка в UI: Site → Path Rules
| Тип | Оператор HAProxy | Пример пути | Совпадает |
|---|---|---|---|
prefix |
path_beg |
/api/ |
/api/users, /api/v2/items |
exact |
path |
/health |
только /health |
regex |
path_reg |
^/api/v[0-9]+/ |
/api/v1/, /api/v2/ |
suffix |
path_end |
.php |
любой путь, заканчивающийся на .php |
POST /api/v1/sites/{siteId}/pathrules
{
"name": "API Proxy",
"enabled": true,
"path": "/api/",
"match_type": "prefix",
"priority": 10,
"action": "proxy",
...
}
| Действие | Описание |
|---|---|
proxy |
Проксировать на указанный upstream |
return |
Вернуть статический HTTP-ответ |
redirect |
Выполнить HTTP-redirect |
{
"action": "proxy",
"upstream_addr": "10.0.0.20:3000",
"upstream_scheme": "http",
"websocket": false,
"grpc": false,
"ssl_enabled": false,
"ssl_verify_none": false,
"ssl_sni": "",
"timeout_connect": 5,
"timeout_server": 60,
"lb_algorithm": "roundrobin",
"retries": 2,
"check_enabled": false
}
| Поле | Тип | Описание |
|---|---|---|
upstream_addr |
string | host:port upstream-сервера |
upstream_scheme |
string | http, https, h2, h2c (для gRPC) |
websocket |
bool | Включить WebSocket-туннель (timeout tunnel) |
grpc |
bool | gRPC режим (h2c, proto h2 на сервере HAProxy) |
ssl_enabled |
bool | TLS к upstream |
ssl_verify_none |
bool | Не проверять TLS-сертификат upstream |
ssl_sni |
string | Явный SNI для TLS-соединения к upstream |
timeout_connect |
int | Таймаут соединения в секундах |
timeout_server |
int | Таймаут ответа сервера в секундах |
timeout_tunnel |
int | Таймаут туннеля (WebSocket/gRPC) в секундах |
lb_algorithm |
string | Алгоритм балансировки (если несколько серверов) |
retries |
int | Число повторных попыток при ошибке |
check_enabled |
bool | Health check для серверов Path Rule |
check_inter |
int | Интервал health check (сек) |
check_fall |
int | Порог перевода сервера в DOWN |
check_rise |
int | Порог возврата сервера в UP |
{
"action": "return",
"return_status": 200,
"return_type": "application/json",
"return_body": "{\"status\":\"ok\"}"
}
| Поле | Описание |
|---|---|
return_status |
HTTP-код ответа |
return_type |
Content-Type заголовок |
return_body |
Тело ответа (строка) |
Применение: healthcheck-эндпоинты, заглушки для временно отключённых маршрутов, статические JSON-ответы.
{
"action": "redirect",
"redirect_url": "https://new-site.example.com/path",
"redirect_code": 301
}
| Поле | Описание |
|---|---|
redirect_url |
URL назначения |
redirect_code |
HTTP-код: 301 (permanent), 302 (found), 307 (temporary), 308 (permanent + method) |
Conditions позволяют применять Path Rule только при выполнении дополнительных условий (кроме совпадения пути).
{
"conditions": [
{
"type": "header",
"name": "X-Internal",
"value": "true",
"negate": false
},
{
"type": "src_cidr",
"value": "10.0.0.0/8",
"negate": false
}
]
}
Все условия объединяются логикой AND — правило применяется, только если все условия выполнены.
| Тип | Поля | Описание |
|---|---|---|
header |
name, value |
Заголовок запроса содержит значение |
src_cidr |
value |
IP клиента входит в CIDR |
method |
value |
HTTP метод (GET, POST, ...) |
query_param |
name, value |
Query-параметр содержит значение |
Поле negate: true инвертирует условие (NOT).
{
"rate_limit": {
"enabled": true,
"http_req_rate": 50,
"conn_rate": 20,
"bytes_in_rate": 524288,
"window": 15,
"deny_status": 429
}
}
Rate limit для Path Rule использует отдельный счётчик (sc2) независимо от глобального rate limit сайта (sc0).
{
"waf_exclude_ids": [942100, 942200, 941100]
}
Список CRS rule ID, которые отключаются только для запросов, совпавших с этим Path Rule. Генерирует Coraza SecRule с REQUEST_URI @beginsWith {path}.
Применение: API-эндпоинты, где тело запроса содержит SQL-подобные данные или специфический формат (например, GraphQL queries).
{
"request_headers": [
{"action": "set", "name": "X-Forwarded-Prefix", "value": "/api"},
{"action": "del", "name": "X-Internal-Token"}
],
"response_headers": [
{"action": "set", "name": "X-Frame-Options", "value": "DENY"},
{"action": "add", "name": "X-Content-Type-Options", "value": "nosniff"}
]
}
| Action | Описание |
|---|---|
set |
Установить заголовок (перезаписывает существующий) |
add |
Добавить заголовок (не удаляет существующий) |
del |
Удалить заголовок |
{
"action": "proxy",
"path": "/ws",
"match_type": "prefix",
"upstream_addr": "10.0.0.5:3001",
"websocket": true,
"timeout_tunnel": 3600
}
При websocket: true HAProxy включает timeout tunnel для поддержки длительных соединений. Никакой специальной настройки proto не требуется — WebSocket-upgrade прозрачен через HTTP/1.1.
timeout_tunnel — время бездействия туннеля в секундах (по умолчанию 3600 = 1 час). Установите 0 для бесконечного туннеля.
{
"action": "proxy",
"path": "/grpc.",
"match_type": "prefix",
"upstream_addr": "10.0.0.5:9090",
"upstream_scheme": "h2c",
"grpc": true,
"timeout_tunnel": 3600
}
При grpc: true или upstream_scheme: "h2"/"h2c" HAProxy добавляет proto h2 в строку server, обеспечивая gRPC через cleartext HTTP/2 (h2c).
Для gRPC с TLS к upstream используйте upstream_scheme: "https" + ssl_enabled: true.
Вместо upstream_addr можно задать список серверов (servers):
{
"action": "proxy",
"lb_algorithm": "leastconn",
"servers": [
{"name": "api-1", "address": "10.0.0.10", "port": 3000, "weight": 10},
{"name": "api-2", "address": "10.0.0.11", "port": 3000, "weight": 10},
{"name": "api-3", "address": "10.0.0.12", "port": 3000, "weight": 5}
]
}
Поля серверов в Path Rule аналогичны полям обычного бэкенда, плюс опции SSL и PROXY protocol.
Path Rules сортируются по полю priority (меньше = выше приоритет). В случае нескольких совпадений побеждает правило с наименьшим priority.
Рекомендованные диапазоны:
1–9 — критические маршруты (блокировки, health checks)10–99 — основные маршруты100–999 — fallback маршрутыПорядок в HAProxy-конфиге:
1. Все http-request return (статические ответы)
2. Все http-request redirect
3. Все http-request track-sc2 (rate limits)
4. use_backend be_ha_waf_api if acme_challenge
5. Все use_backend be_pr_* (path rule proxy)
6. use_backend be_* (основной бэкенд сайта)
UI: Раздел Certificates → кнопка Upload PEM
Принимается PEM-файл, содержащий сертификат (цепочку) + приватный ключ:
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
... (intermediate CA)
-----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
API:
POST /api/v1/certs
Content-Type: application/json
{
"name": "example.com.pem",
"pem_data": "-----BEGIN CERTIFICATE-----\n..."
}
Сертификат сохраняется в БД и записывается в ssl_cert_dir при следующем reload. HAProxy загружает все .pem-файлы из директории.
Загрузка ZIP или TAR.GZ архива с множеством сертификатов. HA-WAF автоматически:
.crt + .key (сопоставляет по CN).pem бандлыUI: Certificates → Import Archive
API:
POST /api/v1/certs/upload-archive?auto_renew=true
Content-Type: application/zip
<тело — сырые байты архива, не multipart>
Импорт выполняется асинхронно, эндпоинт сразу отвечает 202 Accepted с
{"job_id": "..."}. Статус проверяется через:
GET /api/v1/certs/upload/{jobId}
Ответ содержит: status (pending/running/done/failed), done/total, список items с результатом по каждому сертификату.
HA-WAF поддерживает неограниченное количество ACME-аккаунтов. Это позволяет:
Один аккаунт помечается default — он используется при выпуске сертификата без явного указания аккаунта.
Settings → ACME Accounts — страница управления аккаунтами. Прямая ссылка: /#/settings?tab=acme.
Доступные действия:
POST /api/v1/acme/accounts
{
"name": "Production LE",
"email": "ops@example.com",
"challenge_type": "http01",
"staging": false,
"is_default": true
}
| Поле | Тип | Описание |
|---|---|---|
name |
string | Человекочитаемое имя аккаунта |
email |
string | Email для уведомлений Let's Encrypt (обязательно) |
directory_url |
string | ACME directory URL (пусто = Let's Encrypt production) |
staging |
bool | true = Let's Encrypt staging (для тестирования) |
challenge_type |
string | http01 или dns01 |
dns_provider |
string | Имя DNS-провайдера (только для dns01) |
dns_credentials |
object | Credentials для DNS-провайдера (ключи — env-var names) |
is_default |
bool | Назначить этот аккаунт по умолчанию |
Ответ: 201 Created с объектом аккаунта (без приватного ключа).
Staging URL: https://acme-staging-v02.api.letsencrypt.org/directory
GET /api/v1/acme/accounts # список всех аккаунтов
GET /api/v1/acme/accounts/{id} # один аккаунт
PUT /api/v1/acme/accounts/{id} # обновить
DELETE /api/v1/acme/accounts/{id} # удалить
POST /api/v1/acme/accounts/{id}/set-default # назначить дефолтным
PUT-семантика:
name — не сбрасывает ACME-регистрацию (экономит API-лимиты Let's Encrypt)email, directory_url или staging — сбрасывает регистрацию; при следующем выпуске сертификата HA-WAF перерегистрирует аккаунт на ACME-сервере{} в теле запроса означает «не менять»; чтобы обновить отдельные ключи, передай только изменившиесяPOST /api/v1/acme/accounts/{id}/set-default
Ответ: обновлённый объект аккаунта с "is_default": true. Предыдущий default сбрасывается атомарно.
UI: Certificates — раздел ACME Accounts в верхней части страницы покажет текущий default-аккаунт. Кнопка Manage ведёт в Settings → ACME Accounts.
/.well-known/acme-challenge/*send_proxyUI: Кнопка Issue via ACME → ввести домены → выбрать аккаунт → Issue.
POST /api/v1/acme/issue
{
"cert_name": "example.com.pem",
"domains": ["example.com", "www.example.com"],
"auto_renew": true,
"account_id": ""
}
| Поле | Описание |
|---|---|
cert_name |
Имя файла сертификата (по умолчанию: {domain[0]}.pem) |
domains |
Список доменов для сертификата |
auto_renew |
Включить автообновление |
account_id |
ID аккаунта (пусто = использовать default) |
Ответ: 202 Accepted с объектом ACMEJob (см. §50). Статус выпуска можно опросить:
GET /api/v1/acme/jobs/{jobId}
Выпуск для конкретного сайта (все домены сайта автоматически):
POST /api/v1/sites/{siteId}/acme/issue
DNS-01 позволяет выпускать wildcard-сертификаты и работает без публичного доступа на порт 80.
| Провайдер | dns_provider |
Credential-ключи |
|---|---|---|
| Cloudflare | cloudflare |
CF_API_TOKEN |
| AWS Route53 | route53 |
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_HOSTED_ZONE_ID |
| DigitalOcean | digitalocean |
DO_AUTH_TOKEN |
| Hetzner DNS | hetzner |
HETZNER_API_KEY |
| Gandi | gandiv5 |
GANDIV5_PERSONAL_ACCESS_TOKEN |
| PowerDNS | pdns |
PDNS_API_URL, PDNS_API_KEY |
| Yandex Cloud | yandexcloud |
YANDEX_CLOUD_IAM_TOKEN (base64 JSON-ключа), YANDEX_CLOUD_FOLDER_ID |
| Technitium DNS | technitium |
TECHNITIUM_SERVER_BASE_URL, TECHNITIUM_API_TOKEN |
POST /api/v1/acme/accounts
{
"name": "Wildcard via Cloudflare",
"email": "admin@example.com",
"challenge_type": "dns01",
"dns_provider": "cloudflare",
"dns_credentials": {
"CF_API_TOKEN": "your-cloudflare-token"
}
}
Безопасность: credentials хранятся в БД. UI никогда не возвращает сохранённые значения в GET-запросах — при редактировании аккаунта нужно ввести только изменившиеся ключи.
POST /api/v1/acme/issue
{
"cert_name": "example.com-wildcard.pem",
"domains": ["*.example.com", "example.com"],
"auto_renew": true,
"account_id": "<id dns01-аккаунта>"
}
# Создать сервисный аккаунт с ролью dns.editor
# Скачать JSON-ключ и закодировать:
cat key.json | base64 -w 0
# Полученную строку вставить в YANDEX_CLOUD_IAM_TOKEN
HA-WAF каждые 12 часов проверяет все сертификаты с флагом auto_renew: true:
Включить/отключить auto_renew для сертификата:
PATCH /api/v1/certs/{name}
{"auto_renew": true}
В UI — кнопка 🔄 в строке сертификата (синяя = включено, серая = отключено).
Ручное обновление:
POST /api/v1/acme/renew/{name}
Выпуск и перевыпуск сертификатов (/acme/issue, /sites/{siteId}/acme/issue,
/acme/renew/{name}) выполняются асинхронно: эндпоинт сразу отвечает
202 Accepted с объектом job'а, а сама работа с ACME-сервером идёт в
фоне (internal/acme/jobs.go, JobRunner). Это защищает API-запрос от
таймаута при медленном DNS-01 challenge или недоступном ACME-сервере.
GET /api/v1/acme/jobs # все job'ы в памяти, новые сначала
GET /api/v1/acme/jobs/{jobId} # статус конкретного job'а
Статусы: pending → running → done | failed. Типы: issue (ручной
выпуск), renew (перевыпуск), site_issue (массовый выпуск для всех
доменов сайта). JobRunner не даёт запустить второй job для того же
cert_name/сайта параллельно — повторный запрос вернёт 409 Conflict.
Тот же JobRunner управляет и фоновым авто-обновлением (§49) — ручной
запуск через API и автоматический таймер используют один и тот же
механизм постановки в очередь.
Если job выпуска/перевыпуска завершается неудачей, запись об этом
сохраняется отдельно от самого job'а — с историей попыток и
настраиваемым автоповтором (internal/certimport, таблица cert_failures).
GET /api/v1/acme/failures # список
GET /api/v1/acme/failures/{id}
PATCH /api/v1/acme/failures/{id} # изменить auto_retry / next_retry_at
DELETE /api/v1/acme/failures/{id}
POST /api/v1/acme/failures/{id}/retry # повторить немедленно
Статусы записи: pending (ожидает повтора) → resolved (сертификат
успешно выпущен) | cancelled (повторы отключены вручную). Поле
error_code классифицирует причину (напр. dns_challenge_failed,
rate_limited) — используется UI для группировки ошибок в разделе
Certificates → Failures.
По умолчанию HAProxy загружает сертификаты из глобальной ssl_cert_dir. Для конкретного сайта можно указать другую директорию:
PUT /api/v1/sites/{siteId}
{
"ssl_cert_dir": "/etc/ha-waf/certs/tenant-a/"
}
Это позволяет изолировать сертификаты разных тенантов.
UI: Раздел Settings → Users
GET /api/v1/users # список пользователей
POST /api/v1/users # создать
PUT /api/v1/users/{id} # обновить (role и/или password)
DELETE /api/v1/users/{id} # удалить
POST /api/v1/auth/password # сменить СВОЙ пароль (self-service)
POST /api/v1/users
{
"username": "operator",
"password": "secure-password",
"role": "viewer"
}
Смена пароля другому пользователю администратором — через PUT /api/v1/users/{id} с полем password (не отдельный эндпоинт).
Пользователи, ожидающие подтверждения. Аккаунты, созданные через OIDC
self-registration (см. Часть XII: OIDC / SSO),
получают статус pending и не могут войти, пока администратор не назначит
им роль:
GET /api/v1/users/pending # список ожидающих
PUT /api/v1/users/{id}/role # назначить роль → переводит в active
{ "role": "viewer" }
| Роль | GET | POST/PUT/DELETE | Управление пользователями |
|---|---|---|---|
admin |
✅ | ✅ | ✅ |
viewer |
✅ | ❌ | ❌ |
API-ключи предназначены для автоматизации (скрипты, CI/CD, мониторинг).
Создать ключ:
POST /api/v1/users/{userId}/api-keys
{
"name": "Deployment script",
"expires_at": "2027-01-01T00:00:00Z"
}
Ответ содержит сырой ключ — он показывается один раз и нигде не сохраняется:
{
"id": "uuid",
"name": "Deployment script",
"key": "hwaf_a3f8d2...",
"expires_at": "2027-01-01T00:00:00Z"
}
Использование:
X-API-Key: hwaf_a3f8d2...
Список ключей пользователя:
GET /api/v1/users/{userId}/api-keys
Удалить ключ:
DELETE /api/v1/users/{userId}/api-keys/{keyId}
Enterprise-функция: требует лицензию с фичей
oidc(см. Часть XIII: Лицензирование). Без лицензии эта часть неактуальна — публичные роуты входа не регистрируются, а вход остаётся только по логину/паролю.
Провайдер настраивается через Settings → SSO в UI или напрямую через API
(admin-роль):
POST /api/v1/auth/providers
{
"name": "Corporate SSO",
"client_id": "ha-waf",
"client_secret": "...",
"discovery_url": "https://idp.example.com/.well-known/openid-configuration",
"enabled": true
}
discovery_url должен указывать на OpenID Connect discovery document
провайдера (Keycloak, Authentik, Okta, Azure AD и т.п. — любой стандартный
OIDC IdP). client_secret шифруется перед сохранением тем же ключом, что
и source_pass у списков доменов, и никогда не возвращается обратно через
API. Изменение провайдера без указания client_secret в теле запроса
сохраняет прежний секрет.
GET /auth/oidc/providers — публичный роут, отдаёт только {id, name} включённых провайдеров).GET /auth/oidc/{providerId}/login — HA-WAF генерирует PKCE code verifier/challenge и state, кладёт их в HMAC-подписанную cookie (ключ выводится из JWT secret), и редиректит на authorization endpoint провайдера.GET /auth/oidc/{providerId}/callback — HA-WAF обменивает code на токены, проверяет id_token (issuer, audience, подпись), и по sub/email/name делает upsert пользователя.pending, вход отклоняется (редирект на /login?pending=true) до тех пор, пока администратор не назначит роль (см. §53). Уже одобренный пользователь получает обычный HA-WAF JWT и cookie-сессию.OIDCSession (по jti токена) — это нужно для backchannel logout.Оба публичных роута (/auth/oidc/providers — без лимита, /auth/oidc/{id}/login — 10 запросов/мин с IP) находятся вне /api/v1, в отличие от остального API.
HA-WAF реализует OpenID Connect Back-Channel Logout 1.0:
если IdP инициирует логаут (например, администратор IdP принудительно
завершает сессию пользователя), провайдер отправляет POST с
logout_token на:
POST /auth/backchannel-logout (без auth, 5 запросов/мин с IP, вне /api/v1)
HA-WAF извлекает issuer из токена (без полной валидации, только для
поиска провайдера — signature проверяется отдельно), находит
соответствующий OIDCProvider по issuer'у и отзывает все активные
OIDCSession этого пользователя. Отозванная сессия перестаёт проходить
JWT middleware при следующем запросе, даже если сам JWT ещё не истёк.
Если провайдер не найден — эндпоинт всё равно отвечает 200 OK, чтобы не
раскрывать список настроенных провайдеров.
Лицензия — это offline-проверяемый JWT-токен (Ed25519-подпись), не
требующий обращения к внешнему серверу активации. Публичный ключ для
проверки подписи зашит в бинарник HA-WAF. Указывается в config.yaml:
license_key: "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...."
или через переменную окружения HAWAF_LICENSE_KEY (удобно для k8s Secret —
не хранить ключ в ConfigMap открытым текстом).
Без ключа (или при пустом license_key) HA-WAF работает в режиме
Community — все базовые функции (WAF, сайты, сертификаты, rate
limiting, GeoIP, k8sban и т.д.) доступны без ограничений; лицензия нужна
только для отдельных enterprise-фич.
GET /api/v1/license
{
"status": "valid",
"customer": "Acme Corp",
"features": ["oidc"],
"expires_at": "2027-01-01T00:00:00Z",
"days_until_expiry": 180
}
| Статус | Значение |
|---|---|
community |
лицензия не задана |
valid |
активна |
grace |
истекла, но в пределах grace-периода (задаётся при выпуске лицензии) — фичи продолжают работать |
expired |
истекла и grace-период прошёл — enterprise-фичи отключаются |
На момент написания единственная лицензируемая фича — oidc
(SSO-провайдеры, см. Часть XII). Попытка
обратиться к её эндпоинтам без валидной лицензии возвращает
402 Payment Required.
Только для установки в Kubernetes с CNI Cilium. На self-hosted (Docker Compose) — недоступно.
Штатный «WAF IP Ban» (config.haproxy.waf_ban_enabled) банит IP на уровне
HAProxy: после N WAF-блокировок IP получает 429 на все последующие
запросы. Это работает, но каждый забаненный запрос всё равно доходит до
HAProxy — тратится TCP/TLS-хендшейк и цикл обработки запроса. k8sban
зеркалит те же баны в CiliumClusterwideNetworkPolicy, чтобы Cilium
дропал трафик забаненного IP на сетевом уровне (eBPF), до попадания в
под HA-WAF — HAProxy такой пакет вообще не видит.
Два независимых переключателя — нужны оба:
# values.yaml
k8sNetworkBan:
enabled: true # по умолчанию true с версии чарта 0.4.1
config:
k8s_network_ban:
enabled: true
policy_name: ha-waf-ip-ban # опционально
poll_interval: "10s" # опционально
ports: ["80", "443"] # опционально
Включение создаёт ClusterRole/ClusterRoleBinding с правами
get/list/watch/create/update/patch на ciliumclusterwidenetworkpolicies
(cluster-scoped ресурс) и монтирует токен ServiceAccount пода —
это реальное расширение прав, см. SECURITY.md.config.haproxy.waf_ban_enabled: true — без него k8sban ничего не
забанит, даже если чарт-флаг включён.Раз в poll_interval (по умолчанию 10с) каждый под HA-WAF независимо:
show table ha_waf_ban_table через admin socket) — список IP, у которых счётчик
WAF-блокировок достиг порога.CiliumClusterwideNetworkPolicy с именем
policy_name (по умолчанию ha-waf-ip-ban), выставляя
spec.ingressDeny.fromCIDR.spec.ingress с fromEntities: ["all"] на те
же порты — без этого правила ingressDeny перевёл бы под в режим
default-deny для всего входящего трафика, а не только для
забаненных IP (это реальная авария, поймана и исправлена при внедрении
фичи — см. issue-трекер проекта).Почему без leader election. Stick-table у каждого пода своя и не
синхронизируется между репликами (peers-секция в HAProxy не
настроена). Вместо того чтобы выяснять, какой под «владеет» каким баном,
срок жизни каждой записи хранится в JSON-аннотации на самом объекте
(ha-waf.io/ban-expiry: {"1.2.3.4/32": "2026-07-28T22:00:00Z"}). Любой
под на каждом цикле: добавляет свои текущие баны с новым временем
истечения и удаляет из общего списка записи, чей срок истёк — вне
зависимости от того, кто их туда добавил. После падения/пересоздания
пода его баны просто перестают продлеваться и самостоятельно
«вымываются» остальными репликами при следующем истечении TTL.
Посмотреть текущие баны:
kubectl get ciliumclusterwidenetworkpolicies.cilium.io ha-waf-ip-ban -o yaml
Что это не защищает. Дроп происходит на узле, куда пакет уже попал (до userspace HAProxy) — это снимает нагрузку с пода/узла, но не спасает от насыщения аплинк-канала при объёмной атаке (для этого нужен внешний anti-DDoS слой — Cloud Armor/Shield или RTBH у транзитного оператора, вне зоны ответственности HA-WAF).
HA-WAF экспортирует метрики через два endpoint:
| Endpoint | Порт | Описание |
|---|---|---|
:9091/metrics |
9091 | HA-WAF OTel Prometheus bridge (WAF метрики, Go runtime) |
:8405/metrics |
8405 | HAProxy native Prometheus (трафик, сессии, бэкенды) |
| Метрика | Описание |
|---|---|
haproxy_frontend_http_requests_total |
Всего HTTP-запросов по фронтенду |
haproxy_backend_http_responses_total |
Ответы по бэкенду с кодами |
haproxy_server_current_sessions |
Текущие соединения к серверу |
haproxy_server_bytes_in_total |
Входящий трафик к серверу |
haproxy_server_bytes_out_total |
Исходящий трафик от сервера |
Для фильтрации по конкретному сайту используйте label:
proxy="be_<site_id_with_underscores>"
Пример PromQL для запросов к сайту my-app (UUID abc-def):
sum(rate(haproxy_backend_http_responses_total{proxy="be_abc_def"}[5m]))
| Метрика | Labels | Описание |
|---|---|---|
waf_blocks_total |
site_id, action, mode |
Число заблокированных запросов WAF (по любому blocking-действию на этапе запроса) |
waf_requests_total |
site_id, mode |
Общее число запросов, обработанных WAF |
Пер-доменная разбивка по sni не является label метрик waf_*_total — метки намеренно ограничены (site_id/mode), чтобы избежать неограниченной кардинальности и риска для памяти сервиса. Стройте графики по доменам из логов: HAProxy access-логи несут sni (все запросы), лог WAF-события несёт sni для заблокированных — на уровне преобразования лог→метрика (Loki/Prometheus recording rules и т.п.).
HA-WAF поддерживает отправку traces, metrics и logs через OTLP gRPC.
telemetry:
otlp:
endpoint: "otel-collector:4317"
insecure: true
logs:
enabled: true
metrics:
enabled: true
traces:
enabled: true
Дополнительные OTLP-exporters можно настроить через UI (раздел Telemetry) или API:
POST /api/v1/telemetry/exporters
{
"name": "Main collector",
"type": "otlp",
"enabled": true,
"endpoint": "collector.monitoring.svc:4317",
"insecure": true,
"signals": ["logs", "metrics", "traces"]
}
Раздел UI: Metrics
Показывает PromQL-графики за выбранный временной диапазон:
Фильтр по сайту: Выбор конкретного сайта из dropdown скрыт в header дашборда — автоматически добавляет {proxy="be_<id>"} к каждому PromQL-запросу.
Для работы дашборда необходим Prometheus/VictoriaMetrics с метриками HAProxy на
:8405.
При каждом reload HA-WAF создаёт снапшот конфигурации:
haproxy.cfgUI: Раздел Config → Revisions
Список ревизий:
GET /api/v1/config/revisions
Просмотр конкретной ревизии:
GET /api/v1/config/revisions/{id}
Diff между текущим состоянием БД и последней ревизией:
GET /api/v1/config/diff
Возвращает JSON diff с полями added, removed, changed для каждой сущности.
Откат к ревизии:
POST /api/v1/config/revisions/{id}/rollback
Восстанавливает состояние БД из ревизии и выполняет reload. Создаёт новую ревизию.
Статус несохранённых изменений:
GET /api/v1/config/status
# {"has_pending_changes": true}
Отменить несохранённые изменения:
POST /api/v1/config/discard
Экспорт (полный бэкап конфига):
GET /api/v1/export
Возвращает JSON со всеми сущностями. Используйте для бэкапов и переноса конфига между инстансами.
Импорт:
POST /api/v1/import
Content-Type: application/json
{"sites": [...], "certs": [...], ...}
Импорт заменяет существующую конфигурацию. Сделайте экспорт перед импортом.
HA-WAF позволяет задать кастомные HTML-страницы для стандартных HTTP ошибок.
UI: Раздел Error Pages
Поддерживаемые коды: 400, 403, 404, 429, 500, 502, 503, 504
GET /api/v1/error-pages # список
GET /api/v1/error-pages/{code} # получить страницу
PUT /api/v1/error-pages/{code} # сохранить
DELETE /api/v1/error-pages/{code} # сбросить к умолчанию
PUT /api/v1/error-pages/403
{
"code": 403,
"content": "<!DOCTYPE html><html>...</html>"
}
Страницы сохраняются в error_dir (/etc/ha-waf/errors/) и подключаются в HAProxy через директиву errorfile.
UI: Раздел Settings → System
Позволяет переопределить параметры из config.yaml через веб-интерфейс без перезапуска сервиса. Изменения сохраняются в БД и применяются при следующем reload.
Управляемые параметры:
maxconn, таймауты, SSL-опцииGET /api/v1/config
PUT /api/v1/config
{
"maxconn": 100000,
"timeout_connect": 5,
"timeout_client": 30,
"timeout_server": 60
}
HA-WAF включает встроенный anti-bot стек — набор механизмов, встроенных в генерируемый конфиг HAProxy и SPOA-агент Coraza. Стек развивался в два этапа: этап 1 (#157) — quick wins чистым конфигом HAProxy, этап 2 (#158) — IP-reputation, verified bots, challenge и honeypot.
Порядок обработки запроса (фиксирован шаблоном конфига):
whitelist (bypass WAF) → WAF domain bypass → GeoIP → IP-reputation →
IP-blacklist → verified bots → Bot Policy (UA-фильтры) →
Coraza/SPOE (CRS + honeypot/challenge-детект) → WAF-deny →
honeypot-реакция → challenge → rate limits (conn_cur и др.) →
throttle → path rules → backend
Принцип: дешёвые проверки — раньше дорогих. Бот, заблокированный UA-фильтром или reputation-фидом, не расходует WAF-конвейер; challenge стоит после SPOE, потому что его вердикты приходят оттуда же.
Матрица «угроза → механизм»:
| Угроза | Механизм | Глава |
|---|---|---|
| Slowloris, медленный L7-hold | timeout http-request + deny по conn_cur |
77 |
| Простые скрипты/парсеры по UA | Bot Policy | 76 |
| Известные плохие IP/сети | IP-Reputation фиды | 79 |
| Спуфинг Googlebot и других издателей | Verified bots | 80 |
| Credential stuffing, mass scraping | Challenge (redirect → PoW) | 81 |
| Спам форм, агрессивный краулинг | Honeypot + tarpit | 82, 78 |
Глубокая документация по каждому механизму — в каталоге
docs/anti-bot/ (README стека + stage-доки).
UI: механизмы настраиваются вкладками сайта — Политика ботов,
IP-репутация, Челлендж, Ловушки, а conn_cur — в
Ограничениях запросов.
Per-site фильтрация по заголовку User-Agent. Работает на уровне
HAProxy-ACL до Coraza/SPOE — заблокированный бот не расходует WAF.
Вкладка в UI: Site → Политика ботов
PUT /api/v1/sites/{siteId}/bot-policy
{
"enabled": true,
"entries": [
{"pattern": "python-requests", "match_type": "sub", "action": "block", "comment": "скрипты"},
{"pattern": "scrapy", "match_type": "sub", "action": "tarpit", "comment": "скрейпер"},
{"pattern": "UptimeRobot", "match_type": "sub", "action": "allow", "comment": "мониторинг"},
{"pattern": "", "match_type": "empty", "action": "block"}
]
}
| Поле записи | Тип | Описание |
|---|---|---|
pattern |
string | Шаблон UA; обязателен для всех match_type, кроме empty |
match_type |
string | sub — содержит; str — точное совпадение; reg — регулярное выражение; empty — отсутствующий/пустой UA |
action |
string | allow | block | tarpit | decoy | challenge |
comment |
string | Свободный комментарий, в конфиг не попадает |
| Действие | Поведение |
|---|---|
allow |
Известный-хороший бот: выставляет флаг txn.bot_allow, все позже стоящие block/tarpit/decoy его пропускают |
block |
deny с кодом 403 |
tarpit |
Удержание соединения (см. гл. 78) |
decoy |
Нейтральный ответ 200 OK — не раскрывает блокировку (см. гл. 78) |
challenge |
Заглушка: принимается API, но в конфиге не рендерится — используйте политику Challenge (гл. 81) |
Порядок обработки: allow-записи вычисляются первыми, затем
reject-действия. Кроме bot_allow reject-правила обходят: ACME-путь
/.well-known/acme-challenge/ (паттерн вроде bot не ломает выпуск
сертификатов), верифицированные боты (txn.verified_bot, гл. 80) и
WAF-whitelist (txn.waf_bypass).
Валидация паттернов. Паттерны вставляются в конфиг HAProxy без кавычек, поэтому API отвергает пробелы, переводы строк и
#(инъекция директив), а также паттерн с завершающим\(склейка строк). Дляmatch_type=regрегекс проверяется компиляцией Go RE2 — отдельные PCRE-конструкции (lookahead, backreference) допустимы в HAProxy, но API их не пропустит.
Tarpit удерживает слот
maxconnна всё время удержания соединения — используйте только для точечных записей. Decoy отвечает нейтральным 200 OK и не раскрывает блокировку.
Матчинг всех типов регистронезависимый; empty ловит и полностью
отсутствующий заголовок, и User-Agent: нулевой длины.
Типичные паттерны: curl, python-requests, scrapy, wget,
Go-http-client (sub + block); пустой UA (empty + block); агент
мониторинга (UptimeRobot и т.п. — sub + allow). Не блокируйте UA
честных краулеров (Googlebot, bingbot) — спуфинг их UA закрывает
verified bots (гл. 80), а блокировка может отрезать настоящий
поисковый краулер.
Медленные L7-атаки (Slowloris: клиент держит соединения открытыми, досыпая заголовки каплями) закрываются двумя настройками.
Вкладка в UI: Site → Ограничения запросов, блок «Текущие соединения (0 = отключено)», поле «Макс. одновременных».
POST /api/v1/sites/{siteId}/ratelimits
{
"name": "Conn cap",
"enabled": true,
"conn_cur": 15
}
| Поле | Описание |
|---|---|
conn_cur |
Максимум одновременных соединений с одного IP; превышение → deny. 0 = отключено. Поддерживает path_prefixes/path_regex как остальные метрики правила |
Счётчики ведутся в общей stick-table ha_waf_rate_table (ключ sc0,
общий с прочими rate-метриками сайта). Консервативный порог — 10–20;
UI подставляет дефолт 10.
CGNAT. За общим NAT (мобильные операторы, корпоративные сети) сотни легитимных пользователей идут с одного IP — не ставьте порог агрессивно низко. Для сайтов с мобильной аудиторией поднимайте по факту трафика (десятки–сотни).
Вторая линия — глобальный timeout http-request 10s (в defaults
сгенерированного конфига): клиент, не завершивший отправку запроса за
10 секунд, обрывается, не удерживая слот бесконечно.
Ограничения: per-site rate-правила (включая conn_cur) рендерятся на
HTTPS-фронтенде и кастомных HTTP/HTTPS-портах, но не на общем
фронтенде :80 — включайте site-level «Redirect HTTP → HTTPS», чтобы
plain-HTTP-трафик не обходил лимиты. Stick-table имеет тип ip —
счётчики ведутся только для IPv4-клиентов.
Два «тихих» действия антибот-арсенала. Доступны как действия Bot Policy (гл. 76) и как реакция honeypot (гл. 82).
| Действие | Поведение | Когда выбирать |
|---|---|---|
tarpit |
HAProxy принимает запрос и удерживает соединение открытым, не отвечая; кап — timeout tarpit 5s в defaults |
Точечно: надоедливый скрейпер, который повторяет запросы — его время горит, ваши почти нет |
decoy |
Мгновенный 200 OK с нейтральным HTML («OK») |
Бот считает запрос успешным и не мутирует UA ради обхода; не раскрывает факт фильтрации |
Tarpit занимает слот
maxconnна всё время удержания. Массовые tarpit-правила (сотни паттернов, public-сайт под нагрузкой) съедают лимит соединений — держите tarpit для узкого списка. Для массовой фильтрации используйтеblockилиdecoy.
Оба действия наследуют гварды reject-правил Bot Policy: не срабатывают на ACME-пути, для верифицированных ботов, allow-записей и WAF-whitelist.
Блокировка (или логирование) IP из внешних reputation-фидов: Spamhaus DROP, FireHOL, blocklist.de. Фиды скачивает сам HA-WAF в map-файлы; внешних сервисов и ключей не нужно.
Вкладка в UI: Site → IP-репутация (там же — статус снапшотов фидов).
PUT /api/v1/sites/{siteId}/ip-reputation
{
"enabled": true,
"mode": "log",
"sources": ["spamhaus_drop", "firehol_l1"],
"exceptions": ["203.0.113.0/24"]
}
| Поле | Описание |
|---|---|
mode |
log — совпадения только помечаются полем "iprep" в access-логе; enforce — дополнительно deny 403 |
sources |
Подмножество фидов (логика OR — совпадение в любом выбранном): spamhaus_drop, spamhaus_asndrop, firehol_l1, firehol_l2, blocklist_de |
exceptions |
Список CIDR, которые никогда не блокируются по фидам (FP-исключения всегда выигрывают) |
Глобального переключателя нет: фиды скачиваются всегда (≈6 мелких запросов в час, условный GET с ETag), управление — только per-site политиками. Политика стоит в пайплайне сразу после GeoIP, до IP-blacklist и bot-правил.
Статус фидов (внизу вкладки / GET /api/v1/iprep/status): по
каждому источнику — возраст снапшота, количество IP (ips), флаги
stale (снапшот старше 24 ч или отсутствует) и held (новый снапшот
отличался от предыдущего больше чем на ±50 % записей — обновление
отложено, источник держится на старом списке). В enforce-режиме
stale-источники не блокируют (поведение как в log); сайт, у которого все
выбранные источники stale, целиком переходит в log-режим.
Начинайте с
log. Держите режим наблюдения 1–2 недели до перехода наenforce: фид может занести ваш корпоративный диапазон — сразу заведите его вexceptions.
blocklist.de собирается из fail2ban-отчётов и даёт высокий риск ложных срабатываний на CGNAT-соседей. Не используйте как единственный источник.
spamhaus_asndropсейчас парсится в пустой список (фид содержит только ASN без префиксов) — статус честно показываетips: 0.
Юридическая оговорка. HA-WAF не редистрибутирует списки — скачивает их на лету для своего экземпляра и ссылается на первоисточники. Условия использования каждого списка — на сайтах Spamhaus / FireHOL / blocklist.de; для коммерческих применений Spamhaus требует собственного соглашения.
Настоящий Googlebot не должен попадать под UA-фильтры и прочие антибот-механизмы — а спуфер, притворяющийся Googlebot, — должен. Verified bots решают обе задачи двухфакторной проверкой UA-claim × IP:
verified_ranges.map): HA-WAF
раз в сутки качает официальные JSON-списки префиксов (Google
Googlebot/special-crawlers/user-triggered-fetchers, Bing, OpenAI
GPTBot/OAI-SearchBot/ChatGPT-User, Applebot, Anthropic,
PerplexityBot). Недоступный источник сохраняет префиксы прошлого
успешного обновления (fail-open).evilgooglebot.com не
проходит проверку .googlebot.com), и forward-lookup обязан
вернуть исходный IP. Проверки идут вне горячего пути: HAProxy
регистрирует UA-кандидатов в таблице, воркер HA-WAF верифицирует их
через runtime-CLI каждые 10 с.Клиент с IP из диапазонов издателя или positive-вердиктом FCrDNS
получает txn.verified_bot — и снимаются только блокировки
(deny/tarpit/decoy) Bot Policy.
Семантика fail-open: «невозможно проверить ≠ спуфер». Механизм только снимает блокировки, никогда не добавляет их — боты без опубликованных диапазонов и FCrDNS не режутся этим механизмом.
Конфигурация (yaml, дефолты):
bots:
enabled: true # включено по умолчанию; false → конфиг байт-в-байт без фичи
map_file: /etc/ha-waf/verified_ranges.map
sync_interval: 24h
verify_interval: 10s
dns_timeout: 2s
workers: 4
haproxy:
verified_table_port: 19996
Порт 19996 слушает FCrDNS stick-table на 127.0.0.1. Если порт занят на хосте, HAProxy не стартует — переопределите
haproxy.verified_table_port. При включённой фиче HAProxy также поднимаетtune.stick-countersдо 4 (sc0–sc3).
Ограничение: stick-table verified-ботов имеет тип ip — FCrDNS-фактор
работает только для IPv4 (map-фактор покрывает и IPv6-префиксы
издателей).
Двухступенчатый челлендж для подозрительных клиентов (подход Anubis): вместо блокировки — доказательство работы.
/_waf/challenge, API ставит подписанную pending-cookie (HMAC) и
возвращает на исходный URL.Вкладка в UI: Site → Челлендж
PUT /api/v1/sites/{siteId}/challenge
{
"enabled": true,
"mode": "shadow",
"paths": ["/login", "/api/"],
"cookie_ttl_hours": 168,
"rechallenge_pct": 2,
"pow_difficulty": 5,
"require_pow": true
}
| Поле | Диапазон | Описание |
|---|---|---|
mode |
shadow | enforce |
shadow — только вердикты и метрики SPOA, клиентам ничего не отдаётся; enforce — реальный redirect + PoW. Дефолт shadow |
paths |
префиксы /… |
path_beg-префиксы; пусто = все пути сайта |
cookie_ttl_hours |
24–168 (дефолт 168) | Время жизни passed-cookie |
rechallenge_pct |
0–100 (дефолт 1, рекомендация 1–5) | Вероятностный повторный челлендж прошедшего клиента |
pow_difficulty |
4–6 (дефолт 5) | Ведущие нулевые нибблы SHA-256: 4 ≈ лёгкая, 5 ≈ стандарт, 6 ≈ строгая |
require_pow |
bool (дефолт true) |
Требовать PoW в enforce: пустое (no-JS) решение отвергается. Снимайте только осознанно — это a11y-фолбэк ступени 1 |
Автоматически не челленджатся (exemption-цепочка по приоритету):
верифицированные боты (гл. 80), robots.txt и /.well-known/*, фиды
*.xml/*.atom, не-Mozilla UA (curl/git/RSS — JS не исполнят),
WAF-whitelist. Cookie __Host-hw_chal (Secure, HttpOnly, SameSite=Lax)
требует HTTPS.
Enforce требует HTTPS на сайте. Редирект челленджа ведёт на абсолютный
https://-адрес — на HTTP-only сайте это мёртвый эндпоинт, который запирает Mozilla-UA клиентов. API отвергаетmode: enforceдля сайта безhttps_enabled(#172). Включите HTTPS или используйте shadow.
Enforce только на HTTPS-сайтах ≠ «только на HTTPS-порту»: custom HTTP-порт с
waf_enabled=falseне рендерит enforcement, даже если у сайта WAF включён (вердиктов на таком порту не будет).
Челлендж ездит на SPOE-конвейере: сайт с выключенным WAF-политикой
челлендж не получает. При деградации SPOA (агент недоступен или
перегружен) enforce-политики автоматически даунгрейдятся в shadow
(метрика challenge_degraded, гл. 83) — redirect-лупы на живом сайте
не будет.
Секрет cookie — yaml challenge.secret (или env
HAWAF_CHALLENGE_SECRET); пусто → случайный секрет при старте +
предупреждение (cookie протекут на рестарте), для multi-replica
задавайте явно. Смена любой настройки политики инвалидирует все cookie
сайта (policy-hash в подписи).
SEO: оставляйте paths пустыми только осознанно — массовый 302 бьёт
по краулингу; челленджьте избранные префиксы (/search, /login,
/api). robots.txt и фиды уже в exemption-цепочке — не дублируйте.
Скрытые поля-ловушки в формах: поле, которое человек не видит и не заполняет, а бот заполняет. Детект — до CRS в Coraza (через SPOE), реакция — тихая; бан — только повторными срабатываниями через общий WAF gpc0-счётчик. Первый этап выката — shadow (только счётчики).
Вкладка в UI: Site → Ловушки
Workflow владельца сайта:
PUT /api/v1/sites/{siteId}/honeypot
{"enabled": true, "mode": "shadow", "action": "silent200"}
POST /api/v1/sites/{siteId}/honeypot/fields.
Имя генерирует бэкенд: hp_ + 12 hex (криптослучайное).<input type="hidden" name="hp_0123456789ab" value="">
<input type="hidden" name="hp_0123456789ab_token" value="1737050000.9f2c…">
<style>
input[name="hp_0123456789ab"] { position:absolute; left:-9999px; top:-9999px; }
</style>
Первый инпут — ловушка (у человека всегда пустой), второй —
embed-токен бэкенда. Прячьте поле off-screen позиционированием (не
display:none — часть ботов такие поля пропускает), с
tabindex="-1" и aria-hidden="true".POST /api/v1/reload).GET /api/v1/honeypot/status) и
метрика honeypot_hits_total. Накопили статистику — переключите
mode в enforce.Сигналы:
| Сигнал | Условие | Сила |
|---|---|---|
hit |
POST-ом заполнено поле-ловушка, Origin-гейт пройден, HMAC-токен валиден, токену ≥ 2 с | сильный — единственный с реакцией |
fast |
как hit, но токену < 2 с | слабый — только счёт |
forged |
ловушка заполнена в POST с чужим/null/битным Origin (cross-origin авто-POST из браузера жертвы) |
слабый — только счёт, без реакции |
links |
≥ 2 вхождения URL в ARGS | слабый — только счёт |
notoken |
ловушка заполнена, токен отсутствует/бит/чужой | слабый — только счёт (fail-open) |
Реакция (enforce): silent200 — нейтральный 200 OK («бот видит
успех», ловушка не раскрывается) или tarpit (гл. 78). Никогда — 4xx
и никогда прямой бан с первого срабатывания: каждое hit инкрементит
общий gpc0 WAF-бан-счётчик, 429 наступает только при повторах.
Защита от форжа третьей стороной: токен публичен (он вшит в HTML),
поэтому защита — не в секретности, а в Origin-гейте SPOA: браузер
обязан ставить Origin на cross-origin POST и не может подделать его
со страницы. Нет Origin / same-origin → обычный путь hit (клиент без
браузера = бот); чужой/null Origin → сигнал forged, жертва
не тарпитруется и не банится. Query-вектор исключён структурой правила
(матчится только ARGS_POST + метод POST).
robots.txt не нужен. Ловушка — скрытое поле в реальной форме, а не отдельная страница-URL: честные краулеры формы не отправляют и в ловушку не попадают, менять robots.txt для honeypot не требуется. (Классические page-trap'ы — запрещённые в robots.txt ссылки-ловушки — в HA-WAF не реализованы.)
CSS-рефакторинг форм — главный враг ловушки. Если рефакторинг сделает поле видимым (сломалось off-screen-позиционирование, переименование поля), люди начнут его заполнять. Симптом: счётчик срабатываний внезапно растёт после релиза фронтенда при неизменном трафике ботов. Не называйте поле «человеческими» именами — автозаполнение браузеров заполнит его (имя
hp_+hex выбрано специально).
Счётчики /honeypot/status — in-memory, сбрасываются на рестарте
(история — в метрике honeypot_hits_total). Токены не истекают;
смена challenge.secret инвалидирует их (старые станут notoken —
слабым сигналом, не блоком).
Метрики anti-bot стека (Prometheus-эндпоинт control-plane / OTel):
| Метрика | Тип | Значение |
|---|---|---|
challenge_verdicts_total{site_id,verdict} |
counter | Вердикты challenge-cookie: passed | pending | challenge. В shadow-режиме — единственная наблюдаемость |
honeypot_hits_total{site_id,signal} |
counter | Сигналы ловушек: hit (сильный) | fast | notoken | forged | links |
spoa_processing_failures_total{kind} |
counter | SPOE-сообщения, проваленные агентом: abandoned (HAProxy бросил после таймаута) | headers | body |
challenge_degraded |
gauge 0/1 | 1 — enforce-политики challenge даунгрейднуты в shadow из-за деградации SPOA |
GET /api/v1/iprep/status |
API | Статус фидов: updated_at, ips, stale, held per-source |
JA4 (#159, наблюдательная фаза). На терминируемых HTTPS-фронтендах
считается TLS-фингерпринт JA4: поле ja4 в access-логе и заголовок
X-TLS-JA4 бэкенду; метрики с per-fingerprint label нет (кардинальность),
TCP-passthrough и :80 фингерпринта не имеют. Только наблюдение — блокировки
по JA4 (комбо-правила) в будущей фазе #188, детали в
docs/anti-bot/ja4.md.
Деградация SPOA и auto-downgrade. Enforce-challenge зависит от
SPOE-вердиктов. Сценарий «HAProxy жив, вердиктов нет» (перегрузка
агента, рестарт control-plane) даёт бесконечный redirect-луп. Монитор
control-plane каждые 5 с проверяет TCP-доступность SPOA (тот же адрес,
что у health-check'а HAProxy) и долю провалов обработки SPOE-сообщений:
окно 30 с, >50 % провалов при ≥20 обработанных сообщениях → проба
считается неуспешной (низкий трафик перегрузкой не считается).
Гистерезис 3 неудачи/3 успеха (15 с) → challenge_degraded=1, все
enforce-политики рендерятся как shadow; восстановление автоматом, в БД
ничего не меняется.
Что алертить:
challenge_degraded == 1 дольше 5 минут — SPOA перегружен/недоступен;rate(spoa_processing_failures_total) при живом порте агента —
перегрузка с живым TCP (второй сигнал монитора);honeypot_hits_total{signal=hit} после релиза фронтенда —
CSS-регрессия скрытия поля (гл. 82);challenge_verdicts_total{verdict=challenge} у одного
сайта — redirect-луп (у клиента выключены cookies) или слишком
широкие paths;stale: true / held: true — фид не обновляется дольше суток.Типовые проблемы:
| Симптом | Диагноз |
|---|---|
| Enforce-challenge не работает, метрик вердиктов нет | Сайт без WAF-политики (challenge ездит на SPOE) или деградация SPOA (challenge_degraded=1) |
API не принимает mode: enforce |
Сайт без https_enabled — включите HTTPS (#172) |
| IP из фида не блокируется | Источник stale (enforce не работает по stale), IP в exceptions или фид held |
spamhaus_asndrop всегда ips: 0 |
Известное ограничение фида (ASN без префиксов, гл. 79) |
| Счётчики honeypot обнулились | Рестарт control-plane — счётчики in-memory, история в метрике |
| Клиент с выключенными cookies зациклился на челлендже | Осознанный трейд-офф redirect-стадии; наблюдайте долю verdict=challenge |
| conn_cur не срабатывает | Трафик идёт через :80 (лимиты не рендерятся — гл. 77) или клиент IPv6 |
Полный справочник со всеми эндпоинтами, полями тел запросов/ответов и
примерами — docs/api.md. Здесь дублировать его не имеет
смысла: этот список так же выходил из синхронизации с кодом, как и
остальная документация — держать одну точку правды проще, чем две.
Аутентификация — три взаимозаменяемых способа: Authorization: Bearer <JWT>, cookie auth_token (+ X-CSRF-Token для небезопасных методов),
X-API-Key: hwaf_<key>. Подробности — в docs/api.md → Аутентификация.
# ──────────────────────────────────────────────
# REST API server
# ──────────────────────────────────────────────
server:
addr: ":8080" # API + Web UI bind address
# ──────────────────────────────────────────────
# Database
# ──────────────────────────────────────────────
database:
driver: sqlite # sqlite | postgres
dsn: /var/lib/ha-waf/ha-waf.db # path (sqlite) or connstring (postgres)
dsn_read: "" # read-only replica (postgres only)
# ──────────────────────────────────────────────
# HAProxy
# ──────────────────────────────────────────────
haproxy:
binary: /usr/local/sbin/haproxy
config_dir: /etc/ha-waf/haproxy
config_file: /etc/ha-waf/haproxy/haproxy.cfg
pid_file: /var/run/ha-waf/haproxy.pid
socket_path: /var/run/ha-waf/haproxy-admin.sock
# Performance
maxconn: 50000
ulimit_n: 200000
# TLS
ssl_options: "no-sslv3 no-tlsv10 no-tlsv11"
ssl_cert_dir: /etc/ha-waf/certs/
# Ports
http_port: 80
https_port: 443
# Paths
error_dir: /etc/ha-waf/errors
geoip_map_file: /etc/ha-waf/geoip.txt
# Prometheus endpoint (HAProxy native)
metrics_bind: ":8405"
metrics_acls:
- "127.0.0.1"
# Internal rate-limit stick-table
internal_rate_table_port: 19999
# ──────────────────────────────────────────────
# Coraza WAF SPOE
# ──────────────────────────────────────────────
spoa:
addr: "127.0.0.1:9000"
# ──────────────────────────────────────────────
# Authentication
# ──────────────────────────────────────────────
auth:
jwt_secret: "" # empty = random on startup (set explicitly in production)
token_ttl: "24h" # Go duration: 1h, 24h, 168h
# ──────────────────────────────────────────────
# Telemetry
# ──────────────────────────────────────────────
telemetry:
prometheus_addr: ":9091" # empty = disabled
# otlp:
# endpoint: "otel-collector:4317"
# insecure: true
# logs: { enabled: true }
# metrics: { enabled: true }
# traces: { enabled: true }
# ──────────────────────────────────────────────
# k8sban — сетевой бан через Cilium (k8s only, см. Часть XIV)
# ──────────────────────────────────────────────
k8s_network_ban:
enabled: false # false на self-hosted (Docker Compose) — секция игнорируется
policy_name: ha-waf-ip-ban
poll_interval: "10s"
ports: ["80", "443"]
# ──────────────────────────────────────────────
# Верхнеуровневые поля
# ──────────────────────────────────────────────
license_key: "" # см. Часть XIII; можно задать через переменную окружения HAWAF_LICENSE_KEY
log_level: info # debug | info | warn | error
HA-WAF читает основной конфиг только из файла (-config флаг) — переменные
окружения не подставляются в config.yaml шаблонизатором. Единственное
исключение:
| Переменная | Назначение |
|---|---|
HAWAF_LICENSE_KEY |
Переопределяет license_key из файла. Удобно для k8s Secret — ключ не хранится открытым текстом в ConfigMap. |
Флаг запуска:
ha-waf -config /etc/ha-waf/config.yaml
Проверка версии:
ha-waf -version
Health endpoints:
GET /healthz → 200 сразу после запуска (liveness)
GET /readyz → 200 после полной инициализации (readiness)
| Issue | Severity | CWE | Статус | Коммит |
|---|---|---|---|---|
| IDOR в DeleteAPIKey | 🔴 Critical | CWE-639 | ✅ Исправлено | 9587f25 |
| OIDC cookies без Secure flag | 🔴 Critical | CWE-614 | ✅ Исправлено | 09c668d |
| DNS credentials в env vars | 🔴 Critical | CWE-526 | ✅ Исправлено | 806dc59 |
| Open Redirect в OIDC | 🟡 Medium | CWE-601 | ✅ Исправлено | d519d4e |
| Rate limiting на /login | 🟡 Medium | CWE-307 | ✅ Исправлено | a55d620 |
P0 (Completed):
9587f25)09c668d)806dc59)err == → errors.Is() (1c4030d, 37341e0)df5167b)4799a17)41d6ffd)bcde570)a55d620)d519d4e)d12a8b4)a0f8c0e)P1 (Short-term — остались): 6. Rate limiting distributed (Redis) — enhancement 7. Security headers middleware 8. CSP headers
P2 (Medium-term): 9. Code-splitting frontend 10. Refactor god objects