Stick‑tables — одна из самых мощных, но недооценённых возможностей HAProxy. Это встроенное in‑memory хранилище, в котором балансировщик умеет считать метрики (скорости запросов, соединений, байтов), хранить произвольные ключи (IP, cookie, JWT‑идентификаторы), вести счётчики нарушений и принимать решения на лету через ACL. В этой статье соберу практические рецепты для защиты от ботов, настройки rate limit, смягчения L7/L4 DDoS и организации sticky‑sessions без внешних баз.
Что такое stick‑table и чем она отличается от обычных ACL
Обычные ACL в HAProxy смотрят на атрибуты текущего запроса/соединения. Stick‑table добавляет память: мы можем записать ключ (например, IP клиента) и хранить связанный с ним набор метрик и счётчиков, с истечением по времени. Дальше через ACL проверяем накопленные значения: «Сколько запросов за 10 секунд сделал этот IP?», «Сколько раз он промахнулся по паролю?», «К какому бэкенду был привязан его сеанс?». Это фундамент для rate limit, анти‑DDoS, серых списков и sticky‑sessions.
Базовый скелет: отслеживаем соединения и запросы
Начнём с типовой таблицы для IP‑ключей. В ней будем хранить conn_rate и http_req_rate — скорости новых соединений и HTTP‑запросов, а также общий счётчик нарушений gpc0 для эскалации блокировок.
# /etc/haproxy/haproxy.cfg
frontend fe_http
bind :80
mode http
option httplog
# Таблица (ключ: src IP)
stick-table type ip size 200k expire 10m store conn_rate(10s),http_req_rate(10s),gpc0
# Трекинг: соединения в sc1, HTTP-запросы в sc0
tcp-request connection track-sc1 src
http-request track-sc0 src
# Порог на волны новых соединений (L4)
acl burst_conn sc_conn_rate(1) gt 50
tcp-request connection reject if burst_conn
# Порог на частые HTTP-запросы (L7)
acl burst_req sc_http_req_rate(0) gt 100
http-request deny if burst_req
default_backend be_app
backend be_app
server app1 10.0.0.10:8080 check
server app2 10.0.0.11:8080 check
Здесь track-sc0 и track-sc1 просто «приклеивают» текущий коннект/запрос к записи в таблице (по ключу src), а дальше ACL‑ы читают агрегированные счётчики.
Умная эскалация защиты: «серая зона» через gpc0
Жёсткое deny — не всегда лучшее решение: риск зацепить легитимного пользователя с шумной сетью. Более гибкий подход — общая «карма» через gpc0. Наращиваем карму за подозрительные признаки, а блокируем при достижении порога. Так снижаем ложные срабатывания и даём шанс «погасить» карму истечением expire.
frontend fe_http
bind :80
mode http
option httplog
stick-table type ip size 200k expire 30m store http_req_rate(10s),gpc0
http-request track-sc0 src
# Подозрительные паттерны
acl login_bruteforce path_beg -i /login /auth
acl bad_bots hdr_sub(User-Agent) -i curl python bot
acl too_fast sc_http_req_rate(0) gt 60
# Эскалируем карму
http-request sc-inc-gpc0(0) if login_bruteforce
http-request sc-inc-gpc0(0) if bad_bots
http-request sc-inc-gpc0(0) if too_fast
# Триггер блокировки
acl bad_karma sc_get_gpc0(0) gt 10
http-request tarpit if bad_karma
timeout tarpit 5s
default_backend be_app
tarpit задерживает ответ, экономя ресурсы бэкенда и замедляя злоумышленника. Порог и тип реакции — ваше решение: можно вернуть 403 (http-request deny) или увести на заглушку 429.
Rate limit по зонам: общий, по логину, по API
Классический анти‑DDoS включает не один, а несколько независимых лимитов: общий по IP, строгий на /login и более мягкий на API. Разделение на таблицы даёт гибкость без гонок между зонами. Для сравнения подходов на стороне веб‑сервера посмотрите нашу шпаргалку по лимитам в Nginx.
frontend fe_http
bind :80
mode http
# Общий лимит по IP
stick-table type ip size 200k expire 5m store http_req_rate(10s)
http-request track-sc0 src
acl global_burst sc_http_req_rate(0) gt 120
http-request deny status 429 if global_burst
# Отдельный строгий лимит на /login
stick-table type ip size 100k expire 10m store http_req_rate(30s)
acl is_login path_beg -i /login
http-request track-sc1 src if is_login
acl login_burst sc_http_req_rate(1) gt 10
http-request deny status 429 if is_login login_burst
# Лимит на API ключами по токену, а не по IP
stick-table type string size 200k expire 5m store http_req_rate(10s)
acl is_api path_beg -i /api
http-request set-var(tx.api_key) req.hdr(Authorization),field(2, ) if is_api
http-request track-sc2 var(tx.api_key) if is_api
acl api_burst sc_http_req_rate(2) gt 50
http-request deny status 429 if is_api api_burst
default_backend be_app
Ключи могут быть разными: IP (src), значение заголовка, cookie, произвольная строка. Главное — чтобы ключ действительно представлял «субъекта», которого вы хотите ограничивать.

Sticky‑sessions на stick‑tables: без state на бэкенде
Для веб‑приложений, где важна привязка пользователя к одному бэкенду (сессии в памяти, локальные кеши), stick‑tables позволяют реализовать sticky‑sessions без внешнего Redis. Базовая логика: сохраняем сервер, выбранный при первом запросе, и пытаемся матчить следующий запрос по тому же ключу (cookie или IP).
backend be_app
balance roundrobin
cookie SRVID insert indirect nocache
# Таблица сессий по cookie "sid". Храним server_id.
stick-table type string size 1m expire 30m store server_id
# Если есть cookie sid, пытаемся попасть на тот же сервер
stick on req.cook(sid)
stick match req.cook(sid)
stick store-request req.cook(sid)
server app1 10.0.0.10:8080 cookie A check
server app2 10.0.0.11:8080 cookie B check
Если cookie нет — сессия создаётся впервые и сервер выбирается по балансировке; дальше пара «sid → server_id» живёт в таблице 30 минут после последнего обращения. При миграциях рекомендуется уменьшать expire или сбрасывать таблицу, чтобы ускорить переразметку.
Sticky по IP
Быстрый, но рискованный вариант — source affinity по IP. Подходит для сервисов без авторизации и коротких сессий.
backend be_media
balance roundrobin
stick-table type ip size 500k expire 10m store server_id
stick on src
stick match src
stick store-request src
server s1 10.0.0.20:8080 check
server s2 10.0.0.21:8080 check
Минус: за одним NAT‑адресом может сидеть много пользователей, и «горячая» нода получится перегруженной. Для API лучше использовать ключи на уровне приложения (token, клиентский id).
TCP‑фронтенд: защита до TLS/HTTP
Если шлют вал соединений ещё до TLS/HTTP, полезно отрезать их в mode tcp по conn_rate и текущему числу параллельных соединений conn_cur. Это снижает нагрузку на TLS‑стек и CPU.
frontend fe_tcp
bind :443
mode tcp
option tcplog
stick-table type ip size 200k expire 10m store conn_rate(10s),conn_cur,gpc0
tcp-request connection track-sc1 src
acl too_many_new sc_conn_rate(1) gt 80
acl too_many_cur sc_conn_cur(1) gt 200
tcp-request connection reject if too_many_new or too_many_cur
default_backend be_tls
backend be_tls
mode tcp
server tls1 10.0.0.30:443 check
server tls2 10.0.0.31:443 check
В p95‑пиках разумно не «рубить» жёстко, а применять tarpit или вводить серую зону через gpc0, как в HTTP‑примере. Если вы терминируете TLS на балансировщике, заранее подготовьте и автоматизируйте выпуск SSL-сертификаты.
Для продакшн‑инсталляций HAProxy удобнее разворачивать отдельный инстанс на выделенном сервере или на управляемом VDS — так проще контролировать сеть, firewall и обновления.
Реалистичные сигналы «бот или нет»
Одного rate limit недостаточно. Боты и сканеры часто дают дополнительные маркеры. Несколько практичных эвристик, которые хорошо сочетаются со stick‑tables и ACL:
- Необычные методы: всплеск
HEAD/OPTIONSили неожиданныеTRACE. - Аномальный mix путей: частые запросы к несуществующим ресурсам,
/wp-adminна не‑WordPress и т.п. - Подбор логина: много POST на
/login//wp-login.php. - Генерализация по сети: одинаковые User‑Agent с разных IP, но однотипные паттерны.
Все эти сигналы можно переводить в инкременты gpc0 и реагировать при наборе порога. Важно давать «угасание» за счёт expire, чтобы ошибочно попавшие пользователи со временем возвращались к норме.
Синхронизация stick‑tables между инстансами
В высокодоступном кластере важно, чтобы счётчики и сессии ехали между нодами. Для этого в HAProxy есть секция peers. Таблицы, у которых задан атрибут peers, будут реплицироваться.
peers hapx
peer lb1 10.0.0.1:1024
peer lb2 10.0.0.2:1024
frontend fe_http
bind :80
mode http
stick-table type ip size 200k expire 10m store http_req_rate(10s),gpc0 peers hapx
http-request track-sc0 src
# ... ACL и действия ...
Убедитесь, что время на серверах синхронизировано (NTP), порты открыты, а список пар «peer name → ip:port» зеркален на всех узлах. При проблемах репликации счётчики могут расходиться, что приведёт к непредсказуемой блокировке/разблокировке.

Логирование и отладка
Хорошие логи — половина успеха при настройке rate limit и анти‑DDoS. В log-format можно выводить значения счётчиков stick‑tables, чтобы видеть, почему сработало правило.
global
log 127.0.0.1:514 local0
defaults
log global
option httplog
log-format "%ci:%cp %fi:%fp %HM %HP %HU sc_req_rate=%[sc_http_req_rate(0)] sc_conn_rate=%[sc_conn_rate(1)] gpc0=%[sc_get_gpc0(0)] %ST %TR/%Tw/%Tc/%Tt"
Для оперативной диагностики полезен сокет администрирования и команда «show table».
# Включить сокет в глобале (пример):
global
stats socket /run/haproxy.sock mode 660 level admin expose-fd listeners
# Посмотреть таблицу из шелла:
echo "show table fe_http" | socat stdio /run/haproxy.sock
Вы увидите ключи, счётчики и время до истечения. Это помогает проверить, что ACL действительно инкрементируют gpc0, а http_req_rate соответствует ожиданиям.
Паттерны реакций: deny, tarpit, slow‑down, капли на бэкенд
Выбор реакции зависит от профиля трафика и цены ошибки:
- deny 403/429 — быстро и дёшево. Используйте, если уверены в сигнале.
- tarpit — замедление атакующего, минимальный урон бэкенду.
- retries/redispatch — играйте балансировкой при подозрениях, не «ломая» UX.
- shunt на дешёвую заглушку — отдавайте лёгкую страницу с объяснением/ограничением.
Золотое правило: сначала логируйте и «сигнальте», потом переводите в жёсткие блокировки. В бою включайте лимиты постепенно, наблюдая за ошибками и метриками.
Размеры и производительность: сколько памяти ест таблица
На одну запись в stick‑table уходит несколько десятков байт служебных структур плюс размер ключа и поля store. Умножьте на size, добавьте запас на всплески. Для IP‑таблицы 200k записей ориентируйтесь на десятки мегабайт. Несколько рекомендаций:
- Не завышайте
expire— счётчики должны угасать. - Держите отдельные таблицы под разные зоны, чтобы экономить место и изоляцию TTL.
- Следите за
server-stateи «реестром» серверов, если хранитеserver_idдля sticky‑sessions, чтобы не копить «мертвые» соответствия.
Точечные рецепты
Защита формы логина
frontend fe_http
bind :80
mode http
stick-table type ip size 100k expire 30m store http_req_rate(30s),gpc0
http-request track-sc0 src
acl login path_beg -i /login /wp-login.php /auth
acl fast_login sc_http_req_rate(0) gt 10
http-request sc-inc-gpc0(0) if login fast_login
acl bad_karma sc_get_gpc0(0) gt 5
http-request deny status 429 if login bad_karma
default_backend be_app
Лимит скачивания по IP (L7)
Для отдачи файлов полезно ограничивать не только частоту, но и суммарный объём.
frontend fe_dl
bind :80
mode http
stick-table type ip size 200k expire 30m store http_req_rate(10s),bytes_out_rate(1m)
http-request track-sc0 src
acl heavy sc_bytes_out_rate(0) gt 10000000
http-response deny if heavy
default_backend be_files
Снижение нагрузки на API в пике
Вместо отказа — возвращайте 200 с кэшируемым «извинением», если нормальная обработка критична для бэкенда. Для этого создайте отдельный бэкенд‑заглушку и шунтируйте подозрительных.
frontend fe_api
bind :80
mode http
stick-table type ip size 200k expire 5m store http_req_rate(10s)
http-request track-sc0 src
acl peak sc_http_req_rate(0) gt 80
use_backend be_sorry if peak
default_backend be_api
backend be_sorry
http-response set-header Cache-Control max-age=30
http-request deny status 200
Типичные ошибки и как их избегать
- Нет track‑sc: ACL читают
sc_..., а вы не сделалиtrack-scX. В итоге всегда 0. - Не тот индекс: храните в
sc0, а читаетеsc_http_req_rate(1). Следите за соответствием индексов. - Смешивание зон: одна таблица для всего. Разделяйте таблицы: глобал, логин, API.
- Чрезмерные пороги: завышенные значения дают ложное спокойствие. Начинайте ниже и поднимайте по метрикам.
- Отсутствие логов: включайте
log-formatс выводом ключевыхsc_...значений. - Неучтённые NAT‑кластеры: лимиты по IP режут офисы/мобилки. На критичных зонах переходите на ключи приложения.
Проверка конфигурации и безопасный деплой
Перед выкладкой обязательно валидируйте конфиг и делайте мягкую перезагрузку.
haproxy -c -f /etc/haproxy/haproxy.cfg
systemctl reload haproxy
Начинайте в «аудит‑режиме»: логируйте и инкрементируйте gpc0, но не блокируйте. Снимите p95/p99 метрики, посмотрите на реальные профили трафика, и только затем включайте deny/tarpit.
Итоги
Stick‑tables превращают HAProxy из «тупого» балансировщика в умного traffic‑guardian: они дают память и контекст, позволяют реализовать точные ACL и сложную логику эскалации, помогают с sticky‑sessions и снимают пиковые нагрузки через rate limit. Освоив ключевые счётчики (conn_rate, http_req_rate, bytes_out_rate, gpc0) и принципы трекинга, вы сможете строить надёжную защиту от ботов и смягчать DDoS без внешних зависимостей. Главное — логируйте, тестируйте на реальном трафике и включайте ограничения поэтапно.


