Fail2ban [incremental]: Лучше, быстрее, надежнее

в 15:39, , рубрики: ban-time-incr, fail2ban, linux, информационная безопасность, системное администрирование

fail2ban image
Про fail2ban написано уже много, в том числе и на хабре. Эта статья немного о другом — как сделать защиту им еще надежнее и о еще пока неизвестных в широких кругах новых функциях fail2ban. Добавлю сразу — речь пойдет пока про development branch, хотя уже долго проверенный в бою.

Краткое вступление

В большинстве своем fail2ban устанавливается из дистрибутива (как правило это какая-нибудь стабильная старая версия) и настраивается по манам из интернета за несколько минут. Затем годами работает, без вмешательства админа. Нередко даже логи, за которыми вроде как следит fail2ban, не просматриваются.
Так вот, сподвигнуть на написание этого поста меня заставил случай, произошедший с одним сервером моего хорошего знакомого. Классика жанра — пришла абуза, за ней вторая и пошло поехало. Хорошо еще злоумышленник попался ленивый — логи не потер, да и повезло еще крупно, что logrotate был настроен, чтобы хранить логи месяцами.
Оказалось все довольно банально, подобрали пароль к его админской почте, который по совместительству был паролем для ssh (естественно без ключа). Не рут, но судоер, со всеми вытекающими. Первый его вопрос был: как подобрали — у меня же fail2ban там. И вот здесь как раз засада: не все представляют себе, что подбором паролей сегодня занимаются уже не отдельные компьютеры, а целые бот-сети, кстати поумневшие донельзя. Так вот по логам выяснили, что тут как раз такой случай: перебирала бот-сеть, причем на практике выяснившая его настройки в fail2ban (maxRetry=5, findTime=600 и banTime=600). Т.е. чтобы избежать бана, сеть делала 4 попытки в течении 10 минут с каждого IP. На минуточку в сети порядка 10 тысяч уникальных IP = что-то более 5 с половиной миллионов паролей в сутки.
Кроме того его почтовик делал большую глупость — а именно паузу до 10 секунд, при логине с неправильным именем. Т.е. выяснить, что некоторые имена, в том числе admin, реально имеются, этой сетке не составило труда. Далее шел целенаправленный перебор только паролей для имеющихся имен.
Подробнее на «ремонте» останавливаться не буду — это история долгая, и вообще тема для отдельной статьи. Скажу только, что все почистили и все разрешилось малой кровью, да и отделался он практически «легким» испугом.

Так вот, мысль написать статью возникла после того, как мне (частично заслужено) было высказано: «Так ты про такое знал и ничего не сказал, не предупредил. Да еще и решение есть и не поделился. Ну и сволочь ты». Короче, посему посту — быть.

Мой fail2ban

К безопасности «своих» серверов я отношусь чрезвычайно серьезно. Кроме того же fail2ban, всегда кастомного донельзя, у меня там и мониторинг и еще куча всего. Меня просто реально бесит, что из-за бестолковой серой массы, позволяющей брать под контроль бот-сетей свое железо, приходится убивать уйму времени на защиту (и постоянное сопровождение и контроль ее в дальнейшем). Кстати, чтобы минимизировать этот контроль, я и участвую активно в разработке и fail2ban, да и других проектов от безопасности.

Так вот, моя последняя расширенная версия [sebres:ban-time-incr], позволяет вывести этот назойливый зоопарк раз и навсегда (ни или пока они снова не приспособятся). Это фишка довольно часто обсуждалась всем коммюнити, ну как-то руки не доходили. У меня оно жило в виде отдельных скриптов и каких-то кастомных изменений, пока не оформилось в готовый функционал.

Если коротко, то система, запоминая плохие IP адреса, позволяет каждый раз динамически (экспоненциально) увеличивать время блокировки (banTime) в зависимости от количества предыдущих запретов (banCount). При этом также каждый раз уменьшая количество (maxRetry) возможных провальных попыток (failure) до следующего бана. Наглядно это можно увидеть на следующем примере:

2014-09-23 20:05:31,146 fail2ban.observer [named-refused] Increase Ban    XXX.XXX.XX.XXX (10 # 5 days, 8:04:55 -> 2014-09-29 04:10:24)
2014-09-23 20:05:31,120 fail2ban.actions  [named-refused] Ban     XXX.XXX.XX.XXX (_ # 0:15:00 -> 2014-09-23 20:20:29)
2014-09-23 15:30:32,625 fail2ban.actions  [named-refused] Unban   XXX.XXX.XX.XXX
2014-09-20 23:24:14,620 fail2ban.observer [named-refused] Increase Ban    XXX.XXX.XX.XXX (9 # 2 days, 16:06:18 -> 2014-09-23 15:30:31)
2014-09-20 23:24:14,569 fail2ban.actions  [named-refused] Ban     XXX.XXX.XX.XXX (_ # 0:15:00 -> 2014-09-20 23:39:13)
2014-09-20 21:10:36,708 fail2ban.actions  [named-refused] Unban   XXX.XXX.XX.XXX
2014-09-19 13:03:03,377 fail2ban.observer [named-refused] Increase Ban    XXX.XXX.XX.XXX (8 # 1 day, 8:07:34 -> 2014-09-20 21:10:36)
2014-09-19 13:03:03,361 fail2ban.actions  [named-refused] Ban     XXX.XXX.XX.XXX (_ # 0:15:00 -> 2014-09-19 13:18:02)
2014-09-19 12:38:17,743 fail2ban.actions  [named-refused] Unban   XXX.XXX.XX.XXX
2014-09-18 20:13:23,647 fail2ban.observer [named-refused] Increase Ban    XXX.XXX.XX.XXX (7 # 16:24:55 -> 2014-09-19 12:38:17)
2014-09-18 20:13:23,620 fail2ban.actions  [named-refused] Ban     XXX.XXX.XX.XXX (_ # 0:15:00 -> 2014-09-18 20:28:22)
2014-09-18 20:07:06,053 fail2ban.actions  [named-refused] Unban   XXX.XXX.XX.XXX
2014-09-18 12:03:53,282 fail2ban.observer [named-refused] Increase Ban    XXX.XXX.XX.XXX (6 # 8:03:14 -> 2014-09-18 20:07:05)
2014-09-18 12:03:53,266 fail2ban.actions  [named-refused] Ban     XXX.XXX.XX.XXX (_ # 0:15:00 -> 2014-09-18 12:18:51)
2014-09-18 11:22:40,704 fail2ban.actions  [named-refused] Unban   XXX.XXX.XX.XXX
2014-09-18 07:11:12,200 fail2ban.observer [named-refused] Increase Ban    XXX.XXX.XX.XXX (5 # 4:09:43 -> 2014-09-18 11:20:54)
2014-09-18 07:11:12,160 fail2ban.actions  [named-refused] Ban     XXX.XXX.XX.XXX (_ # 0:15:00 -> 2014-09-18 07:26:11)
2014-09-18 06:47:46,618 fail2ban.actions  [named-refused] Unban   XXX.XXX.XX.XXX
2014-09-18 04:37:29,972 fail2ban.observer [named-refused] Increase Ban    XXX.XXX.XX.XXX (4 # 2:02:16 -> 2014-09-18 06:39:44)
2014-09-18 04:37:29,967 fail2ban.actions  [named-refused] Ban     XXX.XXX.XX.XXX (_ # 0:15:00 -> 2014-09-18 04:52:28)
2014-09-18 04:32:49,491 fail2ban.actions  [named-refused] Unban   XXX.XXX.XX.XXX
2014-09-18 02:55:05,706 fail2ban.observer [named-refused] Increase Ban    XXX.XXX.XX.XXX (3 # 1:23:31 -> 2014-09-18 04:18:35)
2014-09-18 02:55:05,698 fail2ban.actions  [named-refused] Ban     XXX.XXX.XX.XXX (_ # 0:15:00 -> 2014-09-18 03:10:04)
2014-09-18 01:18:37,976 fail2ban.actions  [named-refused] Unban   XXX.XXX.XX.XXX
2014-09-18 00:40:09,592 fail2ban.observer [named-refused] Increase Ban    XXX.XXX.XX.XXX (2 # 0:38:30 -> 2014-09-18 01:18:37)
2014-09-18 00:40:09,548 fail2ban.actions  [named-refused] Ban     XXX.XXX.XX.XXX (_ # 0:15:00 -> 2014-09-18 00:55:07)
2014-09-17 22:47:05,872 fail2ban.actions  [named-refused] Unban   XXX.XXX.XX.XXX
2014-09-17 22:32:05,804 fail2ban.actions  [named-refused] Ban     XXX.XXX.XX.XXX (_ # 0:15:00 -> 2014-09-17 22:47:05)

Здесь хорошо заметно, как каждый следующий бан продлевает время блокировки от 15 минут (0:15:00) первый раз, до 5 с лишним дней (5 days, 8:04:55) после десятой блокировки. У меня в базе есть IP у которых «срок» уже — от нескольких месяцев до перманентного бана.

Ниже можно увидеть, как новый функционал отразился на решении собственно банить IP XXX.XXX.XX.XXX. В примере параметр maxRetry установлен равным 5. Так мы видим, что пока IP не признан плохим он был первый раз забанен после 5-ти попыток, второй раз, уже как плохой — после 3-х (каждая попытка была засчитана за 2), третий и т.д. — после 2-х (попытка идет за 3) и четвертый раз забанен сразу после первой попытки (считается сразу за 5-ть):

2014-09-18 04:37:29,155 fail2ban.observer [named-refused] Found XXX.XXX.XX.XXX, bad - 2014-09-18 04:37:28, 3 # -> 5, Ban
2014-09-18 04:37:29,148 fail2ban.filter   [named-refused] Found XXX.XXX.XX.XXX - 2014-09-18 04:37:28
......
2014-09-18 02:55:04,790 fail2ban.observer [named-refused] Found XXX.XXX.XX.XXX, bad - 2014-09-18 02:55:04, 2 # -> 3, Ban
2014-09-18 02:55:04,763 fail2ban.filter   [named-refused] Found XXX.XXX.XX.XXX - 2014-09-18 02:55:04
2014-09-18 02:22:37,683 fail2ban.observer [named-refused] Found XXX.XXX.XX.XXX, bad - 2014-09-18 02:22:37, 2 # -> 3
2014-09-18 02:22:37,648 fail2ban.filter   [named-refused] Found XXX.XXX.XX.XXX - 2014-09-18 02:22:37
......
2014-09-18 00:40:08,908 fail2ban.observer [named-refused] Found XXX.XXX.XX.XXX, bad - 2014-09-18 00:40:08, 1 # -> 2, Ban
2014-09-18 00:40:08,625 fail2ban.filter   [named-refused] Found XXX.XXX.XX.XXX - 2014-09-18 00:40:08
2014-09-17 23:48:54,404 fail2ban.observer [named-refused] Found XXX.XXX.XX.XXX, bad - 2014-09-17 23:48:53, 1 # -> 2
2014-09-17 23:48:54,397 fail2ban.filter   [named-refused] Found XXX.XXX.XX.XXX - 2014-09-17 23:48:53
2014-09-17 22:49:04,647 fail2ban.observer [named-refused] Found XXX.XXX.XX.XXX, bad - 2014-09-17 22:49:03, 1 # -> 2
2014-09-17 22:49:04,620 fail2ban.filter   [named-refused] Found XXX.XXX.XX.XXX - 2014-09-17 22:49:03
......
2014-09-17 22:32:05,593 fail2ban.filter   [named-refused] Found XXX.XXX.XX.XXX - 2014-09-17 22:32:05
2014-09-17 22:06:29,952 fail2ban.filter   [named-refused] Found XXX.XXX.XX.XXX - 2014-09-17 22:06:29
2014-09-17 21:47:43,439 fail2ban.filter   [named-refused] Found XXX.XXX.XX.XXX - 2014-09-17 21:47:42
2014-09-17 20:43:41,490 fail2ban.filter   [named-refused] Found XXX.XXX.XX.XXX - 2014-09-17 20:43:40
2014-09-17 16:44:35,130 fail2ban.filter   [named-refused] Found XXX.XXX.XX.XXX - 2014-09-17 16:44:34

Без этой логики подсчета failure, умные бот-сети научились подстраивать свою работу так, чтобы просто не попадать в бан. Когда я допилил таки и эту логику и выкатил в продакшн, за считанные дни я избавился практически от всей той нечисти, которую привык видеть годами в своих логах. Например, сейчас средний нормальный ежедневный прирост моих auth.log где то в районе 20-50 строк, раньше на некоторых серверах он был в сотни и тысячи раз больше.

Пока что это development branch, лежит pull request-ом, релиз запланирован пока в версии 0.9.2.
Кому интересно, почитать подробнее про реализацию и историю решения можно здесь — Ban time incr by sebres · Pull Request #716 · fail2ban/fail2ban.

Однако пока эта версия ляжет апдейтом на ваш сервер, пройдет еще немало времени — пока релиз выйдет в mainline, пока его в дистрибутивы возьмут… История длинная, например тот же debian все еще использует 0.8.x — собственно поэтому и статья. Так что качаем руками, устанавливаем… профит.

Взять эту версию можно здесь fail2ban-ban-time-incr.zip

Установить его довольно просто. Если у вас уже до того был fail2ban, установленный из дистрибутива, сохраняем из "/etc/fail2ban/" старые «fail2ban.local» и «jail.local» (Ну и лучше старые «fail2ban.conf» и «jail.conf»). Я бы на всякий случай (из-за возможных личных изменениях в filter и action) сохранил бы куда-нибудь весь каталог "/etc/fail2ban/".

Далее сносим старый дистрибутивный fail2ban, например:

sudo service fail2ban stop
sudo apt-get remove fail2ban

Собственно установка:

cd /tmp
unzip ~/downloads/fail2ban-ban-time-incr.zip
cd fail2ban-ban-time-incr/
sudo python setup.py install

Теперь чтобы заработал новый функционал, нужно в вашем jail.local в [default] (либо для каждой конкретной jail) добавить опцию: bantime.increment = true. Пример и описание можно пока найти в "jail.conf".
Некоторое здесь кратко:

  • bantime.rndtime — максимальное время, используется для добавления к banTime случайного времени, для предотвращения «умных» бот-сетей вычислять точное время, когда IP разблокируется снова. Пример bantime.rndtime = 10m
  • bantime.factor — коэффициент для вычисления экспоненты роста для формулы bantime.formula или множителей bantime.multipliers, по умолчанию значение коэффициента 1, что соответствует увеличению времени запрета на 1, 2, 4, 8, 16… Увеличивая этот параметр для некоторых jail, можно увеличивать время блокировки более агрессивно.
  • bantime.formula — используется по умолчанию для вычисления следующего значения времени запрета, значение по умолчанию: bantime.formula = ban.Time * (1<<(ban.Count if ban.Count<20 else 20)) * banFactor
    Тот же рост времени запрета будет достигнут используя множители bantime.multipliers равные 1, 2, 4, 8, 16, 32…
    Пример более агрессивной формулы для фактора «1» и имеет те же значения роста только для фактора равного «2.0 / 2.885385":
    bantime.formula = ban.Time * math.exp(float(ban.Count+1)*banFactor)/math.exp(1*banFactor)
  • bantime.multipliers — параметр может используется вместо формулы дискретно для вычисления следующего значения времени запрета. Значение множителя равное "-1" (может стоять только в конце списка) и заносит адрес в перманентный бан (до ручного разблокирования).
    Пример 1: bantime.multipliers = 1 2 4 8 16 32 64 — увеличивает время запрета на 1, 2, 4,… и если последний ban count был больше последнего индекса мультипликаторов, то будет всегда использован последний множитель (64 в примере), что при факторе равном «1» и оригинальном времени запрета (10 минут) — соответствует 10.6 часам.
    Пример 2: bantime.multipliers = 1 5 30 60 300 720 1440 2880 — может использоваться для небольшого начального времени запрета (bantime = 60) — т.к. увеличение становится более агрессивным, имеем bantime равный: 1 мин, 5 мин, 30 мин, 1 час, 5 часов, 12 часов, 1 день, 2 дня соответственно.

Если во время проб или в продакшн какой-нибудь (хороший) IP случайно многократно улетел в бан (и стал соответственно плохим) он забудется (снова станет «белым») сам по истечении трехкратного dbpurgeage (находится в fail2ban.local), либо если с него руками снять бан, используя:
fail2ban-client set <JAIL> unbanip <IP>

Я для тестирования регулярок использую fail2ban-regex, а для теста работоспособности что-нибудь типа:

logger -t 'test:auth' -i -p auth.info "pam_unix(test:auth): authentication failure; logname= uid=0 euid=0 tty=test ruser=admin rhost=1.2.3.4"

Не забываем про старт:

sudo service fail2ban start

Вот собственно и все, теперь надеюсь ваш сервер стал еще чуточку защищенней. Ну а вы не ленитесь и поглядывайте все-таки в логи (доверяй, но проверяй).

P.S. Стандартная приписка: Fail2Ban is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY. Короче говоря — пользуйтесь на здоровье, но на свой страх и риск…
И да пребудут ваши сервера в безопасности.

P.P.S. Да чуть не забыл, у меня тут назапланировано что-то допилить, что-то уже готово, нужно просто оформить нормально и выложить, так вот — опрос «Что по вашему следовало бы (до)делать в первую очередь».

Автор: sebres

Источник


  1. tmp:

    Не стартует автоматически на rhel даже после исправления заголовка сервиса запуска (#! /bin/bash). Только вручную(

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js