Библиотека, доступная из интернета, будет получать попытки подбора пароля — это не вопрос «если», а вопрос «когда». Flibusta-at-home защищается на двух уровнях: сама блокирует аккаунт и адрес после серии неудач, и пишет лог, по которому fail2ban может забанить источник на уровне файрвола. Эта статья — про второй уровень.

Что делает сама библиотека

Каждая попытка входа (через форму, через HTTP Basic для OPDS и прямых ссылок на файлы) записывается в таблицу login_attempts в Postgres. Если за последние 15 минут накопилось больше 10 неудач для имени пользователя или больше 5 неудач с одного IP, дальнейшие попытки отвергаются с задержкой в 2 секунды, даже если пароль верный. Через 15 минут блокировка снимается сама. Эти же записи видны администратору в разделе «Пользователи → Активность пользователей».

Этого достаточно против неторопливого подбора, но не против потока запросов: каждая отвергнутая попытка всё равно стоит соединения, обращения к БД и двух секунд процесса php-fpm. fail2ban отсекает источник раньше — на файрволе.

Лог попыток входа

Все неудачные попытки дополнительно пишутся в текстовый файл внутри контейнера /cache/login_attempts/flibusta_login_attempts.log, то есть на хосте — <директория cache>/login_attempts/flibusta_login_attempts.log. Формат строки:

[2026-09-17 03:14:15]: user=admin from=203.0.113.7 failure code=1

Коды ошибок:

Код Значение
1 неверный пароль (форма входа)
2 сессия отвергнута из-за смены User-Agent
3 аккаунт заблокирован по числу неудач
4 адрес заблокирован по числу неудач
5 попытка открыть административный раздел без прав администратора
6 неверный пароль по HTTP Basic (OPDS, прямые ссылки на файлы)

Время пишется в часовом поясе контейнера (TZ). Он должен совпадать с часовым поясом хоста, иначе fail2ban сочтёт свежие записи старыми (или из будущего) и не будет банить.

Важно также, откуда берётся адрес в from=. Это REMOTE_ADDR, который nginx передаёт через fastcgi_param. Если перед nginx стоит ещё один прокси или туннель (Cloudflare, VPS с nginx), в лог попадёт адрес этого прокси, и fail2ban забанит его — то есть заблокирует вообще всех. В таких схемах либо настройте real_ip_header в nginx, либо ставьте fail2ban на той машине, где терминируется соединение с клиентом.

Установка fail2ban на хосте

sudo apt install fail2ban      # Debian/Ubuntu
sudo dnf install fail2ban      # Fedora/RHEL

Проверьте, чем по умолчанию банит ваш дистрибутив (iptables или nftables): fail2ban-client get <jail> actions после запуска или переменная banaction в /etc/fail2ban/jail.conf.

Фильтр

Файл /etc/fail2ban/filter.d/flibusta.conf (в репозитории — docs_and_configs/external_services_config/fail2ban.flibusta.filter.conf):

[Definition]
failregex = ^: user=\S* from=<HOST> failure code=\S+$
datepattern = \[%%Y-%%m-%%d %%H:%%M:%%S\]
ignoreregex =

datepattern описывает отметку времени в квадратных скобках; fail2ban вырезает её, и failregex применяется к остатку строки, который начинается с : user=. Проверить фильтр на реальном логе:

fail2ban-regex /opt/flibusta/cache/login_attempts/flibusta_login_attempts.log \
               /etc/fail2ban/filter.d/flibusta.conf

В выводе должно быть Lines: N lines, 0 ignored, N matched.

Jail

Файл /etc/fail2ban/jail.d/flibusta.local (шаблон в репозитории — fail2ban.jail.local.addition.conf, здесь он дополнен):

[flibusta]
enabled   = true
filter    = flibusta
logpath   = /var/log/flibusta/flibusta_login_attempts.log
maxretry  = 5
findtime  = 10m
bantime   = 1h
# библиотека работает в Docker: см. раздел ниже
banaction = iptables-allports
chain     = DOCKER-USER

Шаблон ожидает лог в /var/log/flibusta/. Либо укажите в logpath реальный путь внутри директории cache, либо сделайте символическую ссылку, чтобы не привязывать конфиг fail2ban к расположению библиотеки:

sudo ln -s /opt/flibusta/cache/login_attempts /var/log/flibusta

maxretry = 5 за findtime = 10m согласуется с внутренней блокировкой библиотеки (5 неудач с адреса за 15 минут): fail2ban забанит примерно в тот же момент, когда библиотека начала бы отвергать попытки сама. Можно поставить bantime = 1d или включить bantime.increment = true для рецидивистов.

Особенность: трафик к Docker-контейнерам

Это самый частый источник «fail2ban показывает бан, а запросы всё равно проходят». Стандартные действия fail2ban добавляют правила в цепочку INPUT, но трафик к опубликованным портам контейнеров проходит через FORWARD и цепочки Docker, минуя INPUT. Docker специально оставляет для пользовательских правил цепочку DOCKER-USER, которая проверяется до всех остальных, — именно её и нужно указать в chain.

Если хост использует nftables и banaction = nftables-multiport, эквивалент — задать chain = DOCKER-USER там же; в свежих версиях fail2ban действие nftables понимает параметр chain. Проверить, что правило действительно попало куда надо:

sudo iptables -L DOCKER-USER -n | head      # или: nft list chain ip filter DOCKER-USER

Отдельный случай — reverse proxy не в Docker, а на хосте (nginx из пакетов). Тогда трафик действительно идёт через INPUT, и строку chain нужно убрать.

Запуск и проверка

sudo systemctl enable --now fail2ban
sudo fail2ban-client status flibusta

Ожидаемый вывод: Currently failed, Total failed, Banned IP list. Для проверки «в бою» с другого адреса (например, с телефона через мобильную сеть) введите неверный пароль шесть раз — соединение должно перестать устанавливаться. Снять бан с себя:

sudo fail2ban-client set flibusta unbanip 203.0.113.7

Свой домашний диапазон и адрес, с которого вы администрируете, имеет смысл внести в ignoreip в секции [DEFAULT] jail-конфига, чтобы опечатка в пароле не выкидывала вас из собственной библиотеки.

Ротация лога

Лог только растёт: библиотека в него дописывает, но никогда не чистит. Файл /etc/logrotate.d/flibusta (шаблон в репозитории — logrotate.conf):

/opt/flibusta/cache/login_attempts/flibusta_login_attempts.log {
    size 1M
    missingok
    rotate 3
    compress
    copytruncate
    notifempty
}

Два момента, которые здесь важны:

  • copytruncate обязателен. PHP открывает файл на дозапись при каждой записи и не держит дескриптор, но и никакого сигнала «переоткрой лог» ему послать нельзя. copytruncate копирует содержимое и обнуляет файл на месте, не меняя inode; fail2ban это переживает штатно.
  • Файл принадлежит uid 82. logrotate работает от root и с этим справится, но если вы включите create, укажите владельца: create 0644 82 82.

Ротация по размеру (size 1M) вместо ежедневной выбрана потому, что лог растёт только при атаках; в спокойные недели там будет пусто.

Если библиотека за Tailscale или VPN

В таких схемах внешние адреса до библиотеки не доходят, а банить адреса из 100.64.0.0/10 (это ваши же устройства) — не то, чего хочется. fail2ban там не нужен; внутренней 15-минутной блокировки достаточно, а лог полезен разве что для того, чтобы увидеть, кто из домашних забыл пароль.