Дома у меня стоит сервер, на котором живут полтора десятка сайтов — рабочие тестовые копии, личный сайт, свои сервисы. Перед ним роутер, перед роутером интернет, а в интернете — сканеры, которые находят новый открытый порт за часы. Одна из линий защиты — fail2ban: он читает журнал обращений и банит адреса, которые ведут себя как атакующие. За первые три месяца он дважды подвёл меня так, что хуже было бы только без него. В первый раз забанил меня самого, во второй — 13 дней не банил никого, рапортуя, что всё работает.

Это первая часть цикла «Домашняя инфраструктура».

Авария первая: «ни один сайт не открывается»

Однажды у меня перестал открываться ни один сайт. Сервер при этом был здоров: с него самого всё отвечало 200, контейнеры работали, нагрузки не было. Снаружи — таймаут. Такой признак — «изнутри 200, снаружи тишина» — почти всегда означает фильтр по адресу, и он нашёлся в первой же команде:

sudo iptables -S | grep f2b
-A f2b-traefik-scan -s 198.51.100.77/32 -j DROP

В бане сидел адрес, с которого я работаю. Фильтр «подозрительные пути» ловил любое обращение к /wp-json/wp/v2/users — сканеры так перебирают пользователей WordPress. Но GET /wp-json/wp/v2/users/me?context=edit — штатный запрос админки: редактор при открытии спрашивает, кто вошёл. Четыре таких запроса за полчаса при maxretry 4 — и бан на семь дней.

Почему не помог ignoreip с домашней сетью: мои запросы к сайтам приходили не изнутри, а снаружи, с внешнего адреса. Для fail2ban я был таким же посетителем из интернета.

Что изменили

  • нарушением теперь считается только то, что уже отбил прокси (запрос получил 403 от правила блокировки), и обращения к путям, которых у сайтов нет вовсе: .env, .git, phpmyadmin. Обычный 200 от сайта нарушением не бывает;
  • в ignoreip добавлены все внешние адреса, с которых работаю я;
  • правило проверки нового фильтра: прогнать его по журналу собственных запросов — должно быть 0 совпадений.

Авария вторая: 13 дней тишины

Через две недели я перевёл журнал прокси из текстового формата в JSON — понадобилось для другого дела, читать из него статус кэша. Фильтры fail2ban были написаны под старый формат строки. С этого момента они не совпадали ни с одной строкой.

Узнал я это случайно, разбирая другой вопрос через 13 дней. Цифры за этот срок:

ПериодБанов
первые четыре дня, старый формат журнала69
следующие 13 дней, новый формат0

Прогон фильтра по свежему журналу подтвердил: 13 105 строк, совпадений — 0. А теперь главное: всё это время fail2ban-client status показывал правильный путь к журналу и «работает». Он и правда работал — читал файл и не находил ничего подходящего.

Статус «работает» у fail2ban ничего не говорит о том, ловит ли он что-то. Признак жизни один — растущий счётчик Total failed в статусе джейла или строки Found в его журнале. Ноль за сутки на сервере в интернете — это не тишина, это поломка.

Фильтр под JSON

Прокси пишет ключи по алфавиту, и адрес клиента идёт первым полем — за это и цепляемся. Вторая обязательная часть — шаблон даты, иначе fail2ban не поймёт время строки и окно findtime будет считаться неверно:

[Definition]
failregex   = ^\{"ClientHost":"<HOST>",.*"DownstreamStatus":403,.*"RouterName":"block-[^"]*@file"
datepattern = "time":"%%Y-%%m-%%dT%%H:%%M:%%S
# проверка на живом журнале: смотреть не только итог, но и строку про дату
fail2ban-regex /var/log/traefik/access.log /etc/fail2ban/filter.d/traefik-scan.conf
# Date template hits:
# |- [# of hits] date format
# |  [13105] "time":"Year-Month-DayT24hour:Minute:Second

Число совпадений по дате стоит в квадратных скобках перед шаблоном. Мой скрипт выката сначала искал число перед словом hit, получал ноль и откатывал правильную правку. Прежде чем верить нулю, проверьте, что разбор вывода вообще способен вернуть не ноль.

Ещё четыре способа сделать защиту бумажной

Каждый из них давал ту же картину: статус зелёный, банов нет или баны есть, но не действуют.

  • backend = systemd из настроек дистрибутива по умолчанию молча игнорирует logpath и читает журнал systemd. Для файла журнала нужен backend = auto;
  • бан не доходит до контейнеров. Действие nftables вешает правило на цепочку INPUT, а трафик к опубликованным портам контейнера идёт через FORWARD. Бан виден в статусе, пакеты проходят. Решение — iptables-multiport в цепочке DOCKER-USER;
  • параметр не передаётся. blocktype строкой в джейле не подхватывается, его надо передать действию: iptables-multiport[blocktype=DROP];
  • правила пропадают после перезапуска Docker — Docker пересобирает свои цепочки. Лечится перезапуском fail2ban следом (у нас — ExecStartPost в дополнении к юниту docker).
[traefik-scan]
enabled   = true
backend   = auto
logpath   = /var/log/traefik/access.log
filter    = traefik-scan
maxretry  = 4
findtime  = 30m
bantime   = 7d
action    = iptables-multiport[name=traefik-scan, port="80,443", protocol=tcp, chain=DOCKER-USER, blocktype=DROP]
ignoreip  = 127.0.0.1/8 192.168.0.0/16

Проверка, что бан реально режет: добавить вручную адрес из документационного диапазона и посмотреть, что в DOCKER-USER появилось правило перехода в цепочку джейла. Цепочки f2b-* появляются только при первом бане — их отсутствие у нового джейла ничего не значит.

Порог — по окну, а не по суткам

Однажды в сервер за пять минут ударили 2290 запросов на главную по голому адресу, без имени сайта. Под это завели джейл против потока: 120 ответов 404 за 2 минуты — бан на 6 часов. Порог выбрали не на глаз: прогнали журнал скользящим окном той же длины. Наш собственный пик — 28 запросов за окно, у налёта — 1432. Между ними и стоит порог. Для входа в админку таким же замером вышло 3 настоящих входа за окно против порога 8.

Через неделю после всех правок: джейл сканеров держал 62 адреса, джейл входа в админку — один.

Как проверить свой fail2ban за пять минут

  1. fail2ban-client status <джейл> — смотреть на Total failed, а не на слово «работает»;
  2. fail2ban-regex по живому журналу — совпадения больше нуля, дата распознаётся во всех строках;
  3. fail2ban-client get <джейл> ignoreip — ваши внешние адреса в списке (поиск по *.conf не видит настроек из *.local);
  4. если сайты в Docker — правило бана в DOCKER-USER, а не в INPUT;
  5. новый фильтр — по журналу своих запросов: 0 совпадений;
  6. поменяли формат журнала — сразу перепроверить все фильтры.

Защита на сервере есть, а уверенности нет?

Пришлите конфиг fail2ban и пару строк журнала — проверю, ловит ли он то, что должен, и не забанит ли вас самих.

· полезно?
АЧ
Алексей Чикун веб-разработчик, команда TRIOHEAD

Делаю сайты и сервисы целиком: от вёрстки до сервера. Пишу о том, что проверил на своих проектах.

Комментарии

// загружаю комментарии…

Комментарии публикуются после проверки. Ссылки и HTML не принимаются.