WAF долго оставался покупкой: облачный сервис с оплатой за трафик или appliance «запросите прайс». Тем временем классический self-hosted вариант — ModSecurity — оброс репутацией хрупкого: C-зависимости, плавающие проблемы nginx-коннектора v3 и неторопливая разработка. В 2026 году появилась рабочая альтернатива без этих trade-off: Coraza — движок WAF на Go, понимающий тот же SecLang и OWASP Core Rule Set v4, — и вписывается он в контейнер размером в десятки мегабайт. Это статья вне цикла про Wazuh, но по соседству с ним: в конце заведём блокировки WAF в тот же SIEM.
Содержание
Открыть содержание
Что WAF реально блокирует — а что нет
WAF — это реверс-прокси с сигнатурным анализом HTTP-запросов: он разбирает путь, заголовки, строку параметров и тело, прогоняет их через набор правил и решает — пропустить или отклонить. OWASP CRS, де-факто стандартный набор таких правил, закрывает типовой арсенал:
- SQL-инъекции — пейлоады
union select,' or 1=1, словарь libinjection, - XSS —
<script>в параметрах, обработанные события, обфусцированные векторы, - path traversal —
../../etc/passwdи кодировки, которые его маскируют, - сканеры и эксплойты известных CVE — унифицированные паттерны Probe/Exploit-групп правил.
Чего WAF не решает — важно проговорить честно, иначе он станет ложным чувством безопасности:
- баги бизнес-логики — IDOR «посмотри чужой заказ, подставив его номер» выглядит как совершенно легитимный запрос,
- украденные креды и валидные сессии — сигнатура не отличит вошедшего злоумышленника от вошедшего владельца,
- атаки, не похожие на сигнатуры — целенаправленный эксплойт под ваше API часто не матчится ни одним правилом из коробки.
Отдельно про TLS: в нашей схеме WAF — реверс-прокси, который терминирует шифрование, поэтому правила видят запросы в открытом виде. Следствие простое: не ставьте его после TLS-passthrough балансировщика — там он увидит только шифрованный шум.
WAF — фильтр типовых атак перед приложением, а не замина аудиту, сканированию зависимостей и нормальному access-контролю.
Предыстория: ModSecurity и SecLang
Десятилетиями «self-hosted WAF» означал одно: ModSecurity. Под Apache модуль v2 стабилен, но это легаси-код и отдельный веб-сервер только ради WAF; под nginx коннектор v3 (libmodsecurity) давно известен плавающими segfault’ами и утечками памяти. Развитие проекта замедлилось, а экосистема вокруг — нет: правила OWASP CRS живут своей жизнью и требуют совместимого движка.
Ценное, что ModSecurity оставил после себя, — не код, а SecLang: декларативный язык правил (SecRule, фазы, трансформации, действия), на котором написаны десятки тысяч правил и весь CRS. Поэтому наследники соревнуются не в «своём формате», а в совместимости с ним.
Coraza: движок и обвязка
Coraza — WAF-движок, написанный на Go с нуля, без C-зависимостей. Он понимает SecLang — включая те части v3-синтаксиса, которых давно ждали в ModSecurity — и официально поддерживает OWASP CRS v4. По бенчмаркам ngrok, гонявшего CRS на обоих движках, Coraza на 20–40% быстрее классического ModSecurity под той же нагрузкой. Как библиотека он встраивается куда угодно, а готовые обвязки закрывают основные прокси:
- Caddy-модуль (
coraza-caddy) — WAF становится директивой Caddyfile; наш выбор для примера ниже, - Traefik-плагин — включается аннотацией в контейнере,
- Envoy через WASM-фильтр (
coraza-proxy-wasm) — для service mesh, - HAProxy через SPOA (
coraza-spoa), - свой Go-сервис — движок подключается как библиотека, без прокси вовсе.
Практическое следствие: WAF перестаёт быть отдельным продуктом. Это слой внутри прокси, который у вас и так стоит, — обновляемый, тестируемый и не требующий лицензионного ключа.
Paranoia levels: охват против false positives
CRS регулирует агрессивность одним числом — paranoia level, от 1 до 4. С каждым уровнем включаются дополнительные группы правил: то, что на PL1 осталось незамеченным, на PL2 матчится и блокируется. Плата предсказуема — рост false positives: легитимные JSON-поля, поиск по товарам, фрагменты кода в тикетах начинают ловиться SQLi- и XSS-правилами.
Рабочий цикл тюнинга один и тот же, независимо от уровня:
- стартуют на PL1 и смотрят лог блокировок — что ловится из легитимного,
- каждый false positive гасят точечно — не отключением правила целиком, а исключением для конкретного параметра:
SecRuleRemoveTargetById 942100 "ARGS:q"снимет SQLi-правило только с параметра поиска, - поднимают PL2, когда тюнинг подчистил шум и appetite к охвату ещё есть.
Уровни 3 и 4 существуют, но в проде редки: они рассчитаны на окружения, где шум — приемлемая цена охвата — фиды для PCI DSS-мониторинга или honeypot’ы, где «легитимного» трафика почти нет. Для обычного приложения разумный потолок — PL2 после вылизанных исключений.
Точечные исключения — та дисциплина, которая отличает работающий WAF от выключенного через месяц из-за «он всё ломает».
ModSecurity против Coraza
| ModSecurity | Coraza | |
|---|---|---|
| Язык и зависимости | C, libmodsecurity | Go, без C-зависимостей |
| Язык правил | SecLang (родной) | SecLang-совместимый |
| Интеграции | модуль Apache v2, коннектор nginx v3 | Caddy, Traefik, Envoy (WASM), HAProxy, библиотека |
| OWASP CRS v4 | поддерживается | поддерживается |
| Скорость на CRS | baseline | +20–40% (бенчмарк ngrok) |
| Образ контейнера | сотни МБ | десятки МБ |
Что нужно, чтобы завести WAF перед приложением
Нужны Docker и любое приложение, отвечающее на HTTP. Демо-структура:
waf-demo/
├── compose.yaml
└── waf/
├── Dockerfile
├── Caddyfile
├── coraza.conf
└── crs-tuning.conf
waf/Dockerfile — Caddy, собранный с модулем Coraza, плюс клон CRS:
FROM caddy:2-builder AS builder
RUN xcaddy build --with github.com/corazawaf/coraza-caddy
RUN git clone --depth 1 https://github.com/coreruleset/owasp-modsecurity-crs.git /owasp-crs
FROM caddy:2
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
COPY --from=builder /owasp-crs /etc/caddy/owasp-crs
RUN cp /etc/caddy/owasp-crs/crs-setup.conf.example /etc/caddy/owasp-crs/crs-setup.conf
COPY Caddyfile /etc/caddy/Caddyfile
COPY coraza.conf crs-tuning.conf /etc/caddy/
waf/Caddyfile — WAF первой директивой, upstream задаётся одной переменной окружения:
{
order coraza_waf first
}
waf {
load_owasp_crs /etc/caddy/owasp-crs
include /etc/caddy/coraza.conf
}
:8080 {
coraza_waf waf
reverse_proxy {$UPSTREAM}
}
waf/coraza.conf — включаем движок, подключаем CRS, включаем JSON-аудит блокировок:
SecRuleEngine On
SecRequestBodyAccess On
SecResponseBodyAccess Off
SecRequestBodyLimit 13107200
SecRequestBodyNoFilesLimit 131072
SecRequestBodyLimitAction Reject
# тюнинг ДО подключения CRS: paranoia level и переменные приложения
Include /etc/caddy/crs-tuning.conf
# OWASP CRS v4: setup + правила
Include /etc/caddy/owasp-crs/crs-setup.conf
Include /etc/caddy/owasp-crs/rules/*.conf
# исключения ПОСЛЕ CRS — действуют на уже загруженные правила
# SecRuleRemoveTargetById 942100 "ARGS:q"
# аудит-лог блокировок: одна JSON-строка на транзакцию
SecAuditEngine RelevantOnly
SecAuditLog /dev/stdout
SecAuditLogFormat JSON
waf/crs-tuning.conf:
# 1 — дефолт, 2 — агрессивнее (поднимайте после тюнинга исключений)
SecAction "id:110000,phase:1,pass,t:none,nolog,setvar:tx.paranoia_level=1"
Нюанс с порядком include: SecRuleRemove* и SecRuleUpdateTarget* действуют на уже загруженные правила, поэтому исключения пишут после Include CRS; а переменные вроде paranoia_level — до, чтобы setup CRS подхватил их при старте.
compose.yaml:
services:
app:
image: traefik/whoami
expose:
- "80"
waf:
build: ./waf
environment:
UPSTREAM: app:80
ports:
- "8080:8080"
depends_on:
- app
Запуск — docker compose up -d --build. Замените traefik/whoami на свой образ, а UPSTREAM — на адрес своего приложения: остальное не меняется.
Как проверить, что всё работает
Чистый запрос проходит, SQLi и XSS — отбиваются на лету:
curl -i http://localhost:8080/ # 200 — легитимный запрос
curl -i "http://localhost:8080/?id=1%20OR%201%3D1--" # 403 — SQLi, правило 942100
curl -i "http://localhost:8080/?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E" # 403 — XSS, 941100
В логе контейнера — JSON-транзакции с ID сработавшего правила:
docker compose logs waf | grep -E '942100|941100'
Каждая транзакция несёт client_ip, URI, код ответа и список сработавших правил с их сообщениями — этого хватает, чтобы разобрать любой блок без угадывания. А ответ 200 на чистом запросе ещё и покажет заголовки whoami: видно, что трафик дошёл до приложения, а не был сгенерирован самим прокси.
Бюджет проверяется той же командой: контейнер укладывается в ~150–200 МБ RAM — WAF вписывается в VPS за $10/мес, на котором живёт само приложение.
Куда девать блокировки
Каждый 403 — это сигнал: кто-то зондирует ваше приложение. Ждать, пока его кто-то заметит в логах, не обязательно — в цикле про Wazuh мы уже строили конвейер «JSON-лог → Fluent Bit → syslog-приёмник менеджера» для аудита Kubernetes. Здесь он повторяется один в один: Fluent Bit хвостит лог контейнера WAF и отправляет строки в syslog на 514/tcp.
[INPUT]
Name tail
Path /var/log/containers/*waf*.log
Parser docker
[OUTPUT]
Name syslog
Host WAZUH_MANAGER_IP
Port 514
Format rfc5424
На менеджере JSON-декодер разворачивает транзакцию, а правило цепляется за код ответа — нумерация продолжает серию:
<rule id="100230" level="6">
<decoded_as>json</decoded_as>
<field name="transaction.response.http_code">^403$</field>
<description>waf: request blocked by Coraza — check transaction.messages for rule id</description>
</rule>
Дальше — знакомая механика: уровень, активный ответ на источник при повторах, дашборд. Хосты, аудит кластера и теперь блокировки WAF сходятся в один SIEM.
Итог
WAF «на минималках» — это не enterprise-appliance за пять тысяч долларов в месяц, а один контейнер между пользователем и приложением: Caddy с модулем Coraza, OWASP CRS v4 и час аккуратного тюнининга исключений. Он закрывает 80% типовых веб-атак — SQLi, XSS, traversal, сканеры — за ~150–200 МБ RAM и $0 лицензий, честно не претендуя на баги бизнес-логики и украденные сессии. А блокировки, в свою очередь, — готовые события для SIEM: атака отбита, но осадочек остался — и он у вас в дашборде.