← HA-WAF EN

HA-WAF Handbook

Полное руководство по установке, настройке и эксплуатации HA-WAF.


Содержание


Часть I: Введение

1. Что такое HA-WAF

HA-WAF — это единый исполняемый файл (Go binary), который объединяет:

HA-WAF управляет HAProxy как дочерним процессом: генерирует конфиг, запускает, перезагружает (бесшовно, через SIGUSR2) и взаимодействует через admin socket. WAF работает через SPOE-протокол на локальном TCP-порту.

2. Ключевые возможности

Категория Возможности
Проксирование 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

3. Архитектура (кратко)

Стандартная редакция (HA-WAF):

                        ┌──────────────────────────────────────────┐
                        │              ha-waf process               │
  Клиент ─── :80/443 ──►│                                          │
                        │  HAProxy 3.2 (child) ◄──SPOE──► Coraza  │
  Admin  ─── :8080 ───►│         REST API + Web UI                │
                        │         SQLite / PostgreSQL               │
                        └──────────────────────────────────────────┘

При каждом изменении конфигурации HA-WAF:

  1. Пересчитывает HAProxy-конфиг из шаблонов
  2. Атомарно перезагружает WAF-экземпляры (без потери соединений)
  3. Выполняет seamless reload HAProxy через SIGUSR2

Часть II: Установка

4. Docker Compose (быстрый старт)

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

5. Kubernetes / Helm

Чарт лежит в репозитории по пути 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/.

Чарт создаёт:

При горизонтальном масштабировании используйте PostgreSQL с dsn_read для read replica. SQLite не поддерживает multi-replica.


Часть III: Конфигурационный файл

6. Структура config.yaml

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).

7. Секция server

server:
  addr: ":8080"   # Адрес REST API и Web UI
Параметр Тип По умолчанию Описание
addr string :8080 Адрес прослушивания API-сервера

8. Секция database

database:
  driver: sqlite
  dsn: /var/lib/ha-waf/ha-waf.db

SQLite

database:
  driver: sqlite
  dsn: /var/lib/ha-waf/ha-waf.db

PostgreSQL (один сервер)

database:
  driver: postgres
  dsn: "host=localhost port=5432 user=hawaf password=secret dbname=hawaf sslmode=disable"

PostgreSQL с read replica (Patroni, Citus, RDS)

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)

9. Секция 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

  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

10. Секция spoa

spoa:
  addr: "127.0.0.1:9000"
Параметр По умолчанию Описание
addr 127.0.0.1:9000 TCP-адрес SPOE-сервера Coraza

Не выставляйте SPOA наружу. Это внутренний протокол между HAProxy и Coraza.

11. Секция auth

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 на всех узлах.

12. Секция telemetry

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

Часть IV: Сайты и бэкенды

13. Создание HTTP/HTTPS сайта

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. Запрос направляется к сайту, если:

14. Создание TCP-сайта

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:

  1. Включает tcp-request inspect-delay 5s (ждёт TLS ClientHello)
  2. Читает SNI из req.ssl_sni
  3. Маршрутизирует к соответствующему бэкенду

Если только один сайт на порту — SNI-инспекция не включается (нет overhead).

Сайт без domains и domain_suffixes становится default_backend для данного порта.

Ограничения TCP-режима: WAF, GeoIP, IP-списки, Path Rules, Rate Limits не работают в TCP-режиме — HAProxy не видит HTTP-содержимое.

15. Бэкенды

К каждому сайту можно добавить несколько бэкенд-серверов.

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

16. Алгоритмы балансировки

Значение Описание
roundrobin По кругу (с учётом веса)
leastconn Наименьшее число активных соединений
random Случайный выбор
source По IP клиента (sticky по источнику)
first Всегда первый доступный сервер

17. Списки доменов (Domain Lists)

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
# комментарии игнорируются

Поля Domain List

Поле Описание
source_url URL для загрузки списка (HTTP/HTTPS)
fetch_interval Интервал обновления в секундах
auth_user / auth_password Basic Auth для загрузки
backend_id UUID бэкенда для этой группы доменов (если не задан — используется основной бэкенд сайта)

Часть V: WAF

18. Политика WAF

Каждый сайт имеет одну политику 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": ""
}

19. Режимы WAF

Режим Поведение Когда использовать
detection Логирует нарушения, не блокирует При начальном развёртывании, для изучения ложных срабатываний
prevention Блокирует запросы при нарушениях Продакшн-защита

Рекомендуемый workflow: начните с detection, изучите WAF-события в UI, добавьте исключения для ложных срабатываний, затем переключитесь на prevention.

20. Уровень паранойи (Paranoia Level)

Paranoia Level (PL) определяет, сколько правил CRS активировано. Более высокий уровень = лучшая защита, но больше ложных срабатываний.

PL Правила Описание
1 Базовые Минимум ложных срабатываний, рекомендуется для старта
2 + Средние Умеренная защита
3 + Строгие Высокая защита, требует тонкой настройки исключений
4 + Параноидальные Максимальная защита, высокая вероятность ложных срабатываний

21. Исключения правил (Rule Exclusions)

Исключения позволяют отключить конкретные 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 сработавшего правила.

22. Пользовательские правила (Custom Rules)

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": "..."
}

Поля Custom Rule

Поле Описание
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 поле)

23. События WAF

Раздел 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 из событий для создания точечных исключений.


Часть VI: Фильтрация по GeoIP

24. Настройка GeoIP

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 не найден в карте — разрешить (не блокировать неизвестных).

25. Формат GeoIP-карты

Файл карты: один 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.


Часть VII: IP-списки

26. Белые списки (Whitelist)

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 всё равно проверяет запросы.

27. Чёрные списки (Blacklist)

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)

Часть VIII: Rate Limiting

28. Правила ограничения частоты запросов

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: любое превышение → блокировка).

29. Path-prefix и regex фильтры

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 взаимоисключающие. Если оба пусты — правило применяется ко всем запросам сайта.


Часть IX: Path Rules (nginx location blocks)

30. Обзор Path Rules

Path Rules — аналог location блоков в nginx. Позволяют настроить специальное поведение для конкретных URL-путей L7 сайта:

Вкладка в UI: Site → Path Rules

31. Тип совпадения пути

Тип Оператор 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",
  ...
}

32. Действия (Actions)

Действие Описание
proxy Проксировать на указанный upstream
return Вернуть статический HTTP-ответ
redirect Выполнить HTTP-redirect

33. Проксирование (proxy)

{
  "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

34. Статический ответ (return)

{
  "action": "return",
  "return_status": 200,
  "return_type": "application/json",
  "return_body": "{\"status\":\"ok\"}"
}
Поле Описание
return_status HTTP-код ответа
return_type Content-Type заголовок
return_body Тело ответа (строка)

Применение: healthcheck-эндпоинты, заглушки для временно отключённых маршрутов, статические JSON-ответы.

35. Перенаправление (redirect)

{
  "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)

36. Условия (Conditions)

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).

37. Rate limit для Path Rule

{
  "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).

38. WAF-исключения для Path Rule

{
  "waf_exclude_ids": [942100, 942200, 941100]
}

Список CRS rule ID, которые отключаются только для запросов, совпавших с этим Path Rule. Генерирует Coraza SecRule с REQUEST_URI @beginsWith {path}.

Применение: API-эндпоинты, где тело запроса содержит SQL-подобные данные или специфический формат (например, GraphQL queries).

39. Модификация заголовков

{
  "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 Удалить заголовок

40. WebSocket

{
  "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 для бесконечного туннеля.

41. gRPC

{
  "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.

42. Множество upstream-серверов

Вместо 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.

43. Приоритеты и порядок обработки

Path Rules сортируются по полю priority (меньше = выше приоритет). В случае нескольких совпадений побеждает правило с наименьшим priority.

Рекомендованные диапазоны:

Порядок в 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_* (основной бэкенд сайта)

Часть X: TLS и сертификаты

44. Загрузка сертификата (Upload)

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-файлы из директории.

45. Массовый импорт архива

Загрузка ZIP или TAR.GZ архива с множеством сертификатов. HA-WAF автоматически:

UI: CertificatesImport 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 с результатом по каждому сертификату.

46. ACME-аккаунты — мультиаккаунтная модель

HA-WAF поддерживает неограниченное количество ACME-аккаунтов. Это позволяет:

Один аккаунт помечается default — он используется при выпуске сертификата без явного указания аккаунта.

Управление аккаунтами через UI

Settings → ACME Accounts — страница управления аккаунтами. Прямая ссылка: /#/settings?tab=acme.

Доступные действия:

Создание аккаунта (API)

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-семантика:

Назначение аккаунта по умолчанию

POST /api/v1/acme/accounts/{id}/set-default

Ответ: обновлённый объект аккаунта с "is_default": true. Предыдущий default сбрасывается атомарно.

47. Выпуск сертификата через ACME

Шаг 1: Убедиться, что аккаунт настроен

UI: Certificates — раздел ACME Accounts в верхней части страницы покажет текущий default-аккаунт. Кнопка Manage ведёт в Settings → ACME Accounts.

Шаг 2: Убедиться, что сайт доступен (для HTTP-01)

Шаг 3: Выпустить сертификат

UI: Кнопка 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

48. DNS-01 challenge

DNS-01 позволяет выпускать wildcard-сертификаты и работает без публичного доступа на порт 80.

Поддерживаемые DNS-провайдеры

Провайдер 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

Создание DNS-01 аккаунта

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-запросах — при редактировании аккаунта нужно ввести только изменившиеся ключи.

Выпуск wildcard-сертификата

POST /api/v1/acme/issue
{
  "cert_name": "example.com-wildcard.pem",
  "domains": ["*.example.com", "example.com"],
  "auto_renew": true,
  "account_id": "<id dns01-аккаунта>"
}

Yandex Cloud: подготовка ключа

# Создать сервисный аккаунт с ролью dns.editor
# Скачать JSON-ключ и закодировать:
cat key.json | base64 -w 0
# Полученную строку вставить в YANDEX_CLOUD_IAM_TOKEN

49. Автообновление сертификатов

HA-WAF каждые 12 часов проверяет все сертификаты с флагом auto_renew: true:

Включить/отключить auto_renew для сертификата:

PATCH /api/v1/certs/{name}
{"auto_renew": true}

В UI — кнопка 🔄 в строке сертификата (синяя = включено, серая = отключено).

Ручное обновление:

POST /api/v1/acme/renew/{name}

50. Асинхронные job'ы выпуска (JobRunner)

Выпуск и перевыпуск сертификатов (/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'а

Статусы: pendingrunningdone | failed. Типы: issue (ручной выпуск), renew (перевыпуск), site_issue (массовый выпуск для всех доменов сайта). JobRunner не даёт запустить второй job для того же cert_name/сайта параллельно — повторный запрос вернёт 409 Conflict.

Тот же JobRunner управляет и фоновым авто-обновлением (§49) — ручной запуск через API и автоматический таймер используют один и тот же механизм постановки в очередь.

51. Отслеживание ошибок выпуска (Cert Failures)

Если 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.

52. Per-site переопределение директории сертификатов

По умолчанию HAProxy загружает сертификаты из глобальной ssl_cert_dir. Для конкретного сайта можно указать другую директорию:

PUT /api/v1/sites/{siteId}
{
  "ssl_cert_dir": "/etc/ha-waf/certs/tenant-a/"
}

Это позволяет изолировать сертификаты разных тенантов.


Часть XI: Пользователи и API-ключи

53. Управление пользователями

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" }

54. Роли и права доступа

Роль GET POST/PUT/DELETE Управление пользователями
admin
viewer

55. API-ключи

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}

Часть XII: OIDC / SSO

Enterprise-функция: требует лицензию с фичей oidc (см. Часть XIII: Лицензирование). Без лицензии эта часть неактуальна — публичные роуты входа не регистрируются, а вход остаётся только по логину/паролю.

56. Настройка провайдера

Провайдер настраивается через 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 в теле запроса сохраняет прежний секрет.

57. Поток входа

  1. Пользователь на странице логина видит кнопку провайдера (список берётся из GET /auth/oidc/providers — публичный роут, отдаёт только {id, name} включённых провайдеров).
  2. Переход на GET /auth/oidc/{providerId}/login — HA-WAF генерирует PKCE code verifier/challenge и state, кладёт их в HMAC-подписанную cookie (ключ выводится из JWT secret), и редиректит на authorization endpoint провайдера.
  3. После входа провайдер редиректит обратно на GET /auth/oidc/{providerId}/callback — HA-WAF обменивает code на токены, проверяет id_token (issuer, audience, подпись), и по sub/email/name делает upsert пользователя.
  4. Если пользователь новый — создаётся со статусом pending, вход отклоняется (редирект на /login?pending=true) до тех пор, пока администратор не назначит роль (см. §53). Уже одобренный пользователь получает обычный HA-WAF JWT и cookie-сессию.
  5. Активная сессия сохраняется в таблице OIDCSession (по jti токена) — это нужно для backchannel logout.

Оба публичных роута (/auth/oidc/providers — без лимита, /auth/oidc/{id}/login — 10 запросов/мин с IP) находятся вне /api/v1, в отличие от остального API.

58. Backchannel Logout

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, чтобы не раскрывать список настроенных провайдеров.


Часть XIII: Лицензирование

59. Модель лицензирования

Лицензия — это 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-фич.

60. Статусы и фичи

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.


Часть XIV: k8sban — сетевой бан через Cilium

Только для установки в Kubernetes с CNI Cilium. На self-hosted (Docker Compose) — недоступно.

61. Зачем это нужно

Штатный «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 такой пакет вообще не видит.

62. Включение

Два независимых переключателя — нужны оба:

  1. На уровне чарта (деплой-тайм, даёт RBAC):
    # 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.
  2. На уровне рантайма (тот же тумблер, что и для L7-бана): config.haproxy.waf_ban_enabled: true — без него k8sban ничего не забанит, даже если чарт-флаг включён.

63. Как это работает

Раз в poll_interval (по умолчанию 10с) каждый под HA-WAF независимо:

  1. Читает свою локальную HAProxy stick-table (show table ha_waf_ban_table через admin socket) — список IP, у которых счётчик WAF-блокировок достиг порога.
  2. Сливает эти IP в общий объект CiliumClusterwideNetworkPolicy с именем policy_name (по умолчанию ha-waf-ip-ban), выставляя spec.ingressDeny.fromCIDR.
  3. Одновременно выставляет 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).


Часть XV: Метрики и телеметрия

64. Prometheus метрики

HA-WAF экспортирует метрики через два endpoint:

Endpoint Порт Описание
:9091/metrics 9091 HA-WAF OTel Prometheus bridge (WAF метрики, Go runtime)
:8405/metrics 8405 HAProxy native Prometheus (трафик, сессии, бэкенды)

Ключевые метрики HAProxy

Метрика Описание
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]))

Метрика WAF

Метрика 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 и т.п.).

65. OpenTelemetry (OTLP)

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"]
}

66. Дашборд метрик в UI

Раздел UI: Metrics

Показывает PromQL-графики за выбранный временной диапазон:

Фильтр по сайту: Выбор конкретного сайта из dropdown скрыт в header дашборда — автоматически добавляет {proxy="be_<id>"} к каждому PromQL-запросу.

Для работы дашборда необходим Prometheus/VictoriaMetrics с метриками HAProxy на :8405.


Часть XVI: Конфиг-ревизии

67. Что такое ревизия

При каждом reload HA-WAF создаёт снапшот конфигурации:

UI: Раздел Config → Revisions

68. Просмотр и откат

Список ревизий:

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

69. Экспорт и импорт конфигурации

Экспорт (полный бэкап конфига):

GET /api/v1/export

Возвращает JSON со всеми сущностями. Используйте для бэкапов и переноса конфига между инстансами.

Импорт:

POST /api/v1/import
Content-Type: application/json

{"sites": [...], "certs": [...], ...}

Импорт заменяет существующую конфигурацию. Сделайте экспорт перед импортом.


Часть XVII: Страницы ошибок

70. Кастомные error pages

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.


Часть XVIII: Настройки системы

71. System Config через UI

UI: Раздел Settings → System

Позволяет переопределить параметры из config.yaml через веб-интерфейс без перезапуска сервиса. Изменения сохраняются в БД и применяются при следующем reload.

Управляемые параметры:

GET /api/v1/config
PUT /api/v1/config
{
  "maxconn": 100000,
  "timeout_connect": 5,
  "timeout_client": 30,
  "timeout_server": 60
}

Часть XX: Защита от ботов (Anti-Bot)

75. Обзор стека защиты от ботов

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 — в Ограничениях запросов.

76. Per-site Bot Policy (UA-фильтры)

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), а блокировка может отрезать настоящий поисковый краулер.

77. Защита от Slowloris (conn_cur)

Медленные 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 10sdefaults сгенерированного конфига): клиент, не завершивший отправку запроса за 10 секунд, обрывается, не удерживая слот бесконечно.

Ограничения: per-site rate-правила (включая conn_cur) рендерятся на HTTPS-фронтенде и кастомных HTTP/HTTPS-портах, но не на общем фронтенде :80 — включайте site-level «Redirect HTTP → HTTPS», чтобы plain-HTTP-трафик не обходил лимиты. Stick-table имеет тип ip — счётчики ведутся только для IPv4-клиентов.

78. Tarpit и decoy

Два «тихих» действия антибот-арсенала. Доступны как действия 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.

79. IP-Reputation фиды

Блокировка (или логирование) 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 требует собственного соглашения.

80. Verified bots

Настоящий Googlebot не должен попадать под UA-фильтры и прочие антибот-механизмы — а спуфер, притворяющийся Googlebot, — должен. Verified bots решают обе задачи двухфакторной проверкой UA-claim × IP:

  1. Издательские диапазоны (map-файл 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).
  2. FCrDNS (stick-table): для издателей без публикуемых диапазонов (Яндекс, Amazon, Meta) — forward-confirmed reverse DNS: PTR-имя под суффиксом издателя (с ведущей точкой — 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-префиксы издателей).

81. Challenge (redirect → PoW)

Двухступенчатый челлендж для подозрительных клиентов (подход Anubis): вместо блокировки — доказательство работы.

Вкладка в 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-цепочке — не дублируйте.

82. Honeypot

Скрытые поля-ловушки в формах: поле, которое человек не видит и не заполняет, а бот заполняет. Детект — до CRS в Coraza (через SPOE), реакция — тихая; бан — только повторными срабатываниями через общий WAF gpc0-счётчик. Первый этап выката — shadow (только счётчики).

Вкладка в UI: Site → Ловушки

Workflow владельца сайта:

  1. Включите политику (сначала shadow!):
    PUT /api/v1/sites/{siteId}/honeypot
    {"enabled": true, "mode": "shadow", "action": "silent200"}
    
  2. Создайте поле — POST /api/v1/sites/{siteId}/honeypot/fields. Имя генерирует бэкенд: hp_ + 12 hex (криптослучайное).
  3. Встройте сниппет в форму (кнопка «Показать сниппет» на вкладке):
    <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".
  4. Примените конфигурацию (POST /api/v1/reload).
  5. Наблюдайте: счётчики на вкладке (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 — слабым сигналом, не блоком).

83. Мониторинг и эксплуатация anti-bot

Метрики 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; восстановление автоматом, в БД ничего не меняется.

Что алертить:

Типовые проблемы:

Симптом Диагноз
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

Приложение A: REST API

Полный справочник со всеми эндпоинтами, полями тел запросов/ответов и примерами — docs/api.md. Здесь дублировать его не имеет смысла: этот список так же выходил из синхронизации с кодом, как и остальная документация — держать одну точку правды проще, чем две.

Аутентификация — три взаимозаменяемых способа: Authorization: Bearer <JWT>, cookie auth_token (+ X-CSRF-Token для небезопасных методов), X-API-Key: hwaf_<key>. Подробности — в docs/api.md → Аутентификация.


Приложение B: Справочник по config.yaml

# ──────────────────────────────────────────────
# 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

Приложение C: Переменные окружения

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)

Часть XXI: Известные проблемы безопасности

84. Текущие security issues

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

85. Roadmap исправлений

P0 (Completed):

  1. ✅ Fix IDOR в DeleteAPIKey — добавлен user_id в WHERE clause (9587f25)
  2. ✅ Добавлен Secure flag к OIDC cookies (09c668d)
  3. ✅ Убраны DNS credentials из env vars — Config structs (806dc59)
  4. ✅ Fix error comparison — err ==errors.Is() (1c4030d, 37341e0)
  5. ✅ Fix hash collision — FNV-1a в urlExclusionID (df5167b)
  6. ✅ Dockerfile — запуск от non-root (4799a17)
  7. ✅ CI DinD TLS (41d6ffd)
  8. ✅ JWT → httpOnly cookies (bcde570)
  9. ✅ Rate limiting на /login (a55d620)
  10. ✅ Open Redirect validation (d519d4e)
  11. ✅ Request body limits (d12a8b4)
  12. ✅ CookieSecure default true + rate limiter IP spoof fix (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