Безопасность · fail2ban

Fail2ban для DSM и контейнеров: конкретные джейлы

SSH, Vaultwarden и Nginx Proxy Manager под защитой от перебора паролей — с разбором того, почему бан из контейнера по умолчанию вообще ничего не блокирует.

Зачем ещё и fail2ban, если в DSM есть Auto Block

Встроенный Auto Block (Панель управления → Безопасность → Учётная запись) защищает от перебора паролей только сам DSM — вход в панель управления и SSH. Он ничего не знает о том, что происходит внутри Vaultwarden или Nginx Proxy Manager: для DSM это просто трафик на порт 8080 или 443, а не конкретная неудачная попытка входа в чужой сейф с паролями. Дальше — как прикрыть именно эти сервисы, оставив Auto Block работать своей стороной для самого DSM.

Почему обычный контейнер fail2ban ничего не забанит

Самая частая ошибка в гайдах "fail2ban в Docker" — запустить его в обычном bridge-режиме и удивляться, почему бан не действует. Причина в изоляции сети: правила iptables, которые fail2ban добавляет изнутри контейнера, применяются только к сетевому namespace этого контейнера — а реальный входящий трафик идёт через namespace хоста и эти правила попросту не видит.

⚠ обязательное условие для этой статьи
Чтобы бан реально резал трафик до того, как он доберётся до сервиса, fail2ban должен работать в network_mode: host с capabilities NET_ADMIN и NET_RAW — то есть модифицировать iptables самого NAS, а не свою изолированную копию. Без этого контейнер будет честно писать в лог "Ban 203.0.113.5", но пакеты от этого IP продолжат доходить до Vaultwarden как ни в чём не бывало.

Что понадобится перед стартом

Шаг 1 — готовим логи сервисов к чтению

01Включаем файловый лог у Vaultwarden

По умолчанию Vaultwarden пишет логи только в stdout контейнера, без отдельного файла на диске. Добавьте переменную окружения в его compose-файл, чтобы получить постоянный лог-файл внутри уже примонтированного тома data:

vaultwarden · docker-compose.yml · добавить
    environment:
      LOG_FILE: "/data/vaultwarden.log"
      LOG_LEVEL: "warn"

После перезапуска сервиса лог появится по хостовому пути /volume1/docker/vaultwarden/data/vaultwarden.log — том уже примонтирован из оригинальной статьи, ничего пересоздавать не нужно.

02Логи NPM уже готовы

Nginx Proxy Manager и так пишет access/error логи каждого Proxy Host в /volume1/docker/nginx-proxy-manager/data/logs/ — отдельным файлом на хост, без дополнительной настройки.

Шаг 2 — docker-compose.yml

docker-compose.yml
services:
  fail2ban:
    image: crazymax/fail2ban:latest
    container_name: fail2ban
    restart: unless-stopped
    network_mode: host
    cap_add:
      - NET_ADMIN
      - NET_RAW
    environment:
      TZ: "Europe/Moscow"
      F2B_LOG_LEVEL: "INFO"
      F2B_DB_PURGE_AGE: "30d"
    volumes:
      - /volume1/docker/fail2ban/data:/data
      - /var/log:/host-var-log:ro
      - /volume1/docker/vaultwarden/data:/var/log/vaultwarden:ro
      - /volume1/docker/nginx-proxy-manager/data/logs:/var/log/npm:ro

Каталог /data хранит конфигурацию джейлов, фильтров и SQLite-базу состояния банов — именно его нужно бэкапить и именно в нём редактируются файлы из следующих шагов.

Шаг 3 — джейл для SSH

/volume1/docker/fail2ban/data/jail.d/sshd.local
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /host-var-log/auth.log
maxretry = 5
findtime = 600
bantime = 3600

Фильтр sshd идёт в комплекте с образом — стандартный, менять не нужно. Единственное, что важно проверить самостоятельно — реальный путь к логу авторизации на вашей версии DSM (иногда это /var/log/auth.log, в других сборках — часть общего messages).

Шаг 4 — джейл для Vaultwarden

Vaultwarden пишет неудачные попытки входа в предсказуемом формате, включающем IP клиента — но только если он получает реальный IP от NPM через заголовки X-Real-IP, настроенные ещё в оригинальной статье про Vaultwarden. Фильтр:

/volume1/docker/fail2ban/data/filter.d/vaultwarden.local
[Definition]
failregex = ^.*Username or password is incorrect\. Try again\. IP: \.
ignoreregex =
/volume1/docker/fail2ban/data/jail.d/vaultwarden.local
[vaultwarden]
enabled = true
filter = vaultwarden
logpath = /var/log/vaultwarden/vaultwarden.log
maxretry = 5
findtime = 600
bantime = 86400
action = iptables-allports[name=vaultwarden]
✓ совет перед тем, как доверять фильтру
Формат строк лога меняется между версиями Vaultwarden. Перед тем как полагаться на джейл, вручную сгенерируйте пару неудачных попыток входа и посмотрите реальные строки в vaultwarden.log — регулярное выражение должно совпадать с тем, что там действительно написано, а не с тем, что написано в этой статье.

Шаг 5 — джейл для Nginx Proxy Manager

Панель администратора NPM аутентифицируется через эндпоинт /api/tokens — повторяющиеся неудачные попытки входа оставляют в access-логе соответствующего Proxy Host характерные строки со статусом 401:

/volume1/docker/fail2ban/data/filter.d/npm-auth.local
[Definition]
failregex = ^ .* "POST /api/tokens HTTP/\d\.\d" 401
ignoreregex =
/volume1/docker/fail2ban/data/jail.d/npm-auth.local
[npm-auth]
enabled = true
filter = npm-auth
logpath = /var/log/npm/*_access.log
maxretry = 10
findtime = 600
bantime = 3600

Порог здесь выше, чем у Vaultwarden (10 попыток вместо 5) — панель администратора NPM видит фоновый шум ботов, сканирующих типовые пути на любом веб-сервере в интернете, и слишком чувствительный порог будет банить IP за случайный автоматический скан, а не за целенаправленный перебор.

Проверка банов и снятие блокировки

ssh · admin@nas
docker exec fail2ban fail2ban-client status vaultwarden

Status for the jail: vaultwarden
|- Filter
|  |- Currently failed: 1
|  |- Total failed:     7
|  `- Journal matches:  n/a
`- Actions
   |- Currently banned: 1
   |- Total banned:     1
   `- Banned IP list:   203.0.113.5

# снять бан, если случайно забанили себя
docker exec fail2ban fail2ban-client set vaultwarden unbanip 203.0.113.5
Первый забаненный IP, который стоит проверить, — ваш собственный. Лучше сразу иметь под рукой команду unban, чем разбираться с этим в панике из соседней сети.

Переживаем перезагрузку NAS

После перезагрузки NAS сетевой стек хоста поднимается заново, и правила iptables, добавленные fail2ban, обнуляются вместе с ним — база банов в /data сохраняется, но реальные правила блокировки нужно применить заново. Контейнер с restart: unless-stopped поднимется сам и переприменит правила при старте — но только если Docker-демон и сетевой стек DSM к этому моменту уже полностью готовы.

✓ совет
Если после перезагрузки джейлы не активны сразу — по аналогии с macvlan-шимом из статьи про сетевой high-load, повесьте перезапуск контейнера fail2ban отдельным шагом на триггерное событие «Загрузка» в Панели управления DSM, с небольшой задержкой после старта Docker.

Совместимость со встроенным файрволом DSM

Если в Панели управления → Безопасность → Файрвол у вас включены собственные правила DSM, они тоже управляют iptables того же хоста. На практике оба механизма обычно сосуществуют — fail2ban добавляет свою цепочку и джамп-правило, не трогая остальные, — но после любого сохранения настроек файрвола DSM стоит проверить командой из предыдущего шага, что банlist по-прежнему на месте, а не был случайно сброшен переприменением правил DSM.

Ограничения подхода и ложные срабатывания

Частые проблемы

СимптомВероятная причина
В логе "Ban", но IP всё равно достукивается до сервисаконтейнер запущен не в network_mode: host — банится изолированный namespace, а не хост
Джейл vaultwarden не видит ни одной неудачной попыткине задан LOG_FILE в переменных окружения Vaultwarden, лог пуст
Себя же и забанили с одного IPне настроен ignoreip для собственной локальной подсети/статического адреса
После перезагрузки NAS баны как будто исчезлиiptables хоста сброшен вместе с сетевым стеком — контейнер должен переприменить правила при старте
Джейл npm-auth банит слишком агрессивноmaxretry слишком низкий для фонового шума ботов — увеличьте порог

После того как SSH, Vaultwarden и NPM у каждого под своим джейлом, а собственный IP в whitelist — фоновый шум от автоматических сканеров паролей перестаёт быть заметен в логах вообще, и там остаются только вещи, которые действительно стоит смотреть глазами.