Fail2Ban 的工作方式可以概括为三个步骤:读取日志文件、用正则表达式匹配失败事件、调用防火墙命令封禁来源 IP。如果直接在宿主机上安装,它能轻松访问 /var/log/auth.log 和系统 iptables。但一旦放进容器,日志文件和防火墙操作都处在不同的命名空间里,默认情况下 Fail2Ban 既看不到日志,也封不了 IP。因此容器化 Fail2Ban 的核心不是把二进制装进镜像,而是设计一条能够穿透容器边界的日志与防火墙链路。

容器化与传统部署的核心差异
传统部署中,Fail2Ban 默认从 /var/log/auth.log、/var/log/nginx/access.log 等位置读取日志,这些路径对宿主机进程完全透明。容器化后,应用日志往往写入容器的 stdout 或者容器内路径,Fail2Ban 容器无法直接访问其他容器的文件系统。即使通过挂载卷映射宿主机日志目录,也必须把宿主机日志目录挂载到 Fail2Ban 容器内,并且挂载的路径要与 jail.local 中的 logpath 一致。这个路径一致性经常被忽略,导致 jail 启动后一直报日志文件不存在。
另一个容易被忽视的差异是防火墙操作。Fail2Ban 默认的 action 会调用 iptables 命令,但容器内的 iptables 规则只作用于容器自身的网络命名空间,对宿主机和其他容器没有影响。要让封禁真正生效,要么让 Fail2Ban 容器使用 host 网络模式,要么通过特权容器加 NET_ADMIN 能力直接操作宿主机网络命名空间。前者简单直接,但需要容器与宿主机共享网络栈;后者更灵活,但配置复杂,还要考虑容器内是否有完整的 iptables 工具链。
状态持久化也是差异之一。Fail2Ban 在内存中维护封禁列表,容器重启后列表丢失。如果恶意 IP 在重启后立刻重新攻击,保护会出现空窗。因此需要把 /var/lib/fail2ban 目录挂载为持久卷,保存 sqlite 数据库或封禁状态。否则每次容器重建,之前的封禁记录全部清空,攻击者可以反复尝试。
日志链路与网络命名空间的打通方式
日志链路的打通通常有三种做法。第一种是把宿主机日志目录只读挂载进容器,例如将宿主机的 /var/log 挂载到容器的 /host_logs,然后修改 jail.local 中 logpath 指向新路径。这种方式最适合监控宿主机上的 sshd、nginx 等传统服务。第二种是收集容器日志到宿主机文件,再通过挂载让 Fail2Ban 读取。比如使用 docker logging driver 把容器日志输出到宿主机 json 文件,再配合正则解析,但解析 json 格式日志比纯文本更复杂。第三种是共享 Docker socket,让 Fail2Ban 直接读取容器日志流,不过这需要额外的脚本或插件,稳定性不如文件挂载。
网络命名空间方面,最直接的是使用 host 网络模式。容器启动时加 --network host,Fail2Ban 调用的 iptables 命令会直接作用于宿主机。但 host 模式意味着容器与宿主机共享 IP 和端口,可能带来端口冲突。如果不想使用 host 模式,可以保持 bridge 网络,同时给容器增加 --cap-add NET_ADMIN 和 --cap-add NET_RAW。在 Linux 上,拥有 NET_ADMIN 能力的容器可以修改宿主机网络命名空间中的 iptables 规则,但需要容器内安装的 iptables 版本与宿主机内核兼容。推荐使用 nftables 动作,通过 nft 命令添加表链,兼容性更好。
如果既不想用 host 模式,也不愿给过高权限,还可以让 Fail2Ban 通过 SSH 到宿主机执行封禁命令,或者调用宿主机上的 HTTP API。这种方案安全性更好,但实现复杂度明显增加,通常只在多租户环境中使用。大多数单机场景下,host 网络加只读日志挂载已经足够。
编写适合容器的 jail 配置与动作
容器内的 Fail2Ban 默认配置通常无法直接使用,因为发行版路径不同。例如 Alpine 镜像中日志文件可能位于 /var/log/messages,而 Debian 系的 auth.log 路径在 Alpine 中不存在。因此需要准备自定义的 jail.local 文件,并明确指定后端为 polling,避免依赖 inotify 事件在挂载卷上可能失效的问题。挂载卷上的文件变化不会可靠触发 inotify,所以 polling 模式是容器环境的默认选择。
下面给出一个基础配置示例,定义一个 sshd jail,指向挂载的宿主机日志 /host_logs/auth.log,封禁动作使用 iptables-multiport,同时设置 bantime、findtime、maxretry。如果使用 nftables,可以把 banaction 改为 nftables-multiport,并确保容器内安装了 nft 命令。对于 Docker 容器产生的登录失败日志,如果日志格式不同,还需要自定义 filter 正则。可以新建 filter.d/custom-app.conf,在 failregex 中匹配容器输出的时间戳和错误信息。
[DEFAULT] bantime = 3600 findtime = 600 maxretry = 5 banaction = iptables-multiport backend = polling [sshd] enabled = true port = 22 filter = sshd logpath = /host_logs/auth.log
动作方面,默认的 iptables 动作会向 INPUT 链插入 DROP 规则,但如果是桥接网络容器,封禁规则不会影响宿主机转发到其他容器的流量。如果目标服务运行在宿主机,host 模式加默认动作足够。如果目标服务是 Docker 容器,则需要在宿主机 FORWARD 链或 DOCKER-USER 链上做封禁,这时要自定义 action 文件,指定插入链的位置。常见的做法是新建 action.d/docker-user.conf,在 actionban 中调用 iptables -I DOCKER-USER -s <ip> -j DROP。需要注意的是,DOCKER-USER 链在 Docker 重启后会保留,适合放置用户自定义规则。
构建镜像与运行示例
构建镜像最简单的方式是基于 Alpine,体积小、安装快。Dockerfile 中安装 fail2ban 和 bash,然后把自定义配置文件拷贝进去。需要特别注意 Alpine 的 fail2ban 包可能不包含所有 action,可以通过 apk add fail2ban 安装,再检查 /etc/fail2ban/action.d 目录。如果缺少 nftables 动作,需要额外安装 nftables 包。以下是一个可用的 Dockerfile 示例。
FROM alpine:3.20
RUN apk add --no-cache fail2ban bash \
&& mkdir -p /var/log
COPY jail.local /etc/fail2ban/jail.local
COPY filter.d /etc/fail2ban/filter.d
ENTRYPOINT ["fail2ban-server", "-f", "-x", "start"]
运行容器时,建议使用 docker-compose 管理参数,避免手动输入长命令。下面给出一个 compose 文件示例,设置了 host 网络、cap_add 和卷挂载。如果不想使用 host 网络,可以去掉 network_mode,改用 cap_add,但需要自定义动作确保规则写入宿主机。卷挂载中宿主机日志目录按需调整,例如只挂载 /var/log/auth.log 或整个 /var/log,注意只读挂载可以防止容器意外修改宿主机日志。
services:
fail2ban:
build: .
network_mode: host
cap_add:
- NET_ADMIN
- NET_RAW
volumes:
- /var/log:/host_logs:ro
- fail2ban_data:/var/lib/fail2ban
restart: unless-stopped
volumes:
fail2ban_data:
启动后验证需要分两步。先执行 docker exec fail2ban fail2ban-client status sshd,查看 jail 是否激活;再模拟失败登录,确认封禁动作生效。如果使用 host 模式,可以在宿主机执行 iptables -L -n 查看新增的 DROP 规则。对于 nftables,则执行 nft list ruleset。如果发现规则没有出现在宿主机上,大概率是容器没有获得正确的网络能力,或者 action 中插入的链名与宿主机实际链名不一致。
容器化 Fail2Ban 的部署难点集中在日志可见性和网络权限两者之间。只要把宿主机日志以只读方式挂载,并选择 host 网络或合适的 cap_add,再配合持久化状态目录,就能获得与传统部署相近的防护效果。对于生产环境,建议将配置纳入版本控制,并通过健康检查监控 jail 状态,避免容器运行中因日志轮转导致文件句柄失效。