OSEN-НИЙ SAAALEСкидка 50% на виртуальный хостинг и VDS
до 30.11.2025 Подробнее
Выберите продукт

Stick‑tables в HAProxy: защита от ботов, лимиты и сессии

Разбираем stick‑tables в HAProxy на практике: отслеживание IP и сессий, rate limiting, анти‑DDoS и защита логина. Покажу рабочие ACL‑паттерны, эскалацию через gpc0/gpc1, sticky‑sessions и синхронизацию таблиц между инстансами. Добавим логи, отладку через admin socket и советы по безопасному деплою.
Stick‑tables в HAProxy: защита от ботов, лимиты и сессии

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, произвольная строка. Главное — чтобы ключ действительно представлял «субъекта», которого вы хотите ограничивать.

Схема: поток ACL и счётчиков stick‑tables в HAProxy

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» зеркален на всех узлах. При проблемах репликации счётчики могут расходиться, что приведёт к непредсказуемой блокировке/разблокировке.

Отладка HAProxy через admin socket: вывод show table

Логирование и отладка

Хорошие логи — половина успеха при настройке 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 без внешних зависимостей. Главное — логируйте, тестируйте на реальном трафике и включайте ограничения поэтапно.

Поделиться статьей

Вам будет интересно

MetalLB и K3s на VDS: L2 vs BGP, healthchecks и устойчивые Service IP OpenAI Статья написана AI (GPT 5)

MetalLB и K3s на VDS: L2 vs BGP, healthchecks и устойчивые Service IP

Как получить стабильные публичные Service IP в K3s на VDS? Разбираем MetalLB в режимах layer2 и BGP: когда что выбирать, ограничен ...
ACME tls-alpn-01 и порт 80: как правильно выпустить SSL за обратным прокси OpenAI Статья написана AI (GPT 5)

ACME tls-alpn-01 и порт 80: как правильно выпустить SSL за обратным прокси

Многие путают tls-alpn-01 с http-01 и запускают проверку на 80-м — валидация падает. Разбираем, почему челлендж обязателен на 443 ...
JWT в Nginx с njs: проверка HS256 и RS256 на реверс‑прокси OpenAI Статья написана AI (GPT 5)

JWT в Nginx с njs: проверка HS256 и RS256 на реверс‑прокси

Практическое руководство для админов и DevOps: как валидировать JWT прямо на уровне Nginx с помощью njs. Разберём HS256 и RS256, с ...