Дома у меня стоит сервер, на котором живут полтора десятка сайтов — рабочие тестовые копии, личный сайт, свои сервисы. Перед ним роутер, перед роутером интернет, а в интернете — сканеры, которые находят новый открытый порт за часы. Одна из линий защиты — 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 за пять минут
fail2ban-client status <джейл>— смотреть наTotal failed, а не на слово «работает»;fail2ban-regexпо живому журналу — совпадения больше нуля, дата распознаётся во всех строках;fail2ban-client get <джейл> ignoreip— ваши внешние адреса в списке (поиск по*.confне видит настроек из*.local);- если сайты в Docker — правило бана в
DOCKER-USER, а не в INPUT; - новый фильтр — по журналу своих запросов: 0 совпадений;
- поменяли формат журнала — сразу перепроверить все фильтры.
Защита на сервере есть, а уверенности нет?
Пришлите конфиг fail2ban и пару строк журнала — проверю, ловит ли он то, что должен, и не забанит ли вас самих.
// загружаю комментарии…