给 Podman 邮局配置 Fail2ban 防 SASL 暴力破解
背景
自建邮局(Postfix + Dovecot)部署在 netcup VPS 上,用 Podman rootless 容器运行(docker-mailserver 镜像)。某天查看 Postfix 日志摘要,发现 24 小时内:
- 2477 封邮件被拒(99%),其中 2471 封来自同一 IP 的垃圾邮件
- 152 次 SASL LOGIN authentication failed,来自全球上百个不同 IP
- Shodan 扫描、TLS 探测、中继滥用尝试若干
虽然 Postfix 自身的反垃圾规则把 99% 的攻击都挡在了门外,没有一封垃圾邮件真正投递,但 SASL 暴力破解是真实威胁——只要密码够弱,迟早会被猜中。fail2ban 可以在 IP 失败若干次后自动通过防火墙封掉它,从源头切断攻击。
环境信息
| 项目 | 值 |
|---|---|
| 宿主机 OS | Arch Linux |
| 容器运行时 | Podman(rootless,用户 nite) |
| 邮局镜像 | ghcr.io/docker-mailserver/docker-mailserver:latest |
| 容器名 | systemd-mailserver |
| Quadlet 文件 | ~/pod/docker-mailserver/mailserver.container |
| 邮件日志路径(宿主机) | ~/pod/docker-mailserver/logs/mail.log |
| 邮件日志路径(容器内) | /var/log/mail/mail.log |
| fail2ban | 宿主机 pacman -S fail2ban,版本 1.1.0 |
| 防火墙 | UFW(不影响 fail2ban) |
关键问题:日志在哪?
fail2ban 需要读取日志文件来检测攻击模式。在传统部署中,Postfix 日志写到 /var/log/mail.log,fail2ban 直接读就行。但在 Podman 容器环境中,有两个选择:
- journald 后端:如果容器日志通过 journald 输出,fail2ban 可以用
backend = systemd从 journald 读取 - polling 后端:如果容器把日志写到挂载的文件中,fail2ban 用
backend = polling直接读文件
本文的邮局容器通过 Volume 挂载把日志写到了宿主机的 ~/pod/docker-mailserver/logs/mail.log,所以选 polling 后端。
注意:Arch Linux 的 fail2ban 默认通过
paths-arch.conf把postfix_backend设为systemd。如果不在 jail 配置中显式覆盖为polling,fail2ban 会去读 journald 而不是文件,结果是什么都匹配不到。
安装 fail2ban
sudo pacman -S fail2ban
sudo systemctl enable --now fail2ban安装后默认只有 sshd jail 启用。运行 sudo fail2ban-client status 确认:
Status
|- Number of jail: 1
`- Jail list: sshd创建 jail 配置
文件路径:/etc/fail2ban/jail.d/mailserver.local
[DEFAULT]
postfix_backend = polling
postfix_log = /home/nite/pod/docker-mailserver/logs/mail.log
[postfix-sasl]
enabled = true
mode = auth
port = smtp,465,submission,imap,imaps,pop3,pop3s
logpath = /home/nite/pod/docker-mailserver/logs/mail.log
backend = polling
maxretry = 5
findtime = 10m
bantime = 1h
banaction = iptables-allports
[postfix]
enabled = true
mode = more
port = smtp,465,submission
logpath = /home/nite/pod/docker-mailserver/logs/mail.log
backend = polling
maxretry = 5
findtime = 10m
bantime = 1h
banaction = iptables-allports各参数说明
| 参数 | 值 | 说明 |
|---|---|---|
mode = auth |
postfix-sasl 用 | 专门匹配 SASL 认证失败(postfix[mode=auth] 是 fail2ban 0.10+ 替代旧 postfix-sasl 过滤器的新写法) |
mode = more |
postfix 用 | 比默认 normal 模式更激进,额外匹配中继滥用、未知主机等 |
backend = polling |
两者都用 | 轮询读取文件日志,不用 journald |
maxretry = 5 |
10 分钟内 5 次失败触发封禁 | |
findtime = 10m |
统计窗口 | |
bantime = 1h |
封禁时长 | |
banaction = iptables-allports |
封禁方式 | 被封 IP 的所有端口全部 REJECT |
postfix-saslvspostfix[mode=auth]:fail2ban 0.10.0 起移除了独立的postfix-sasl过滤器,改为postfix过滤器的mode=auth子模式。在 jail 配置中,[postfix-sasl]section 仍然可用,因为它内部会调用filter = postfix[mode=auth](通过mode = auth参数传递)。
重启并验证
# 测试配置语法
sudo fail2ban-client -t
# 重启
sudo systemctl restart fail2ban
# 查看所有 jail
sudo fail2ban-client status输出应该显示三个 jail:
Status
|- Number of jail: 3
`- Jail list: postfix, postfix-sasl, sshd检查 jail 详情:
sudo fail2ban-client status postfix-saslStatus for the jail: postfix-sasl
|- Filter
| |- Currently failed: 3
| |- Total failed: 3
| `- File list: /home/nite/pod/docker-mailserver/logs/mail.log
`- Actions
|- Currently banned: 0
|- Total banned: 0
`- Banned IP list:File list 正确指向容器日志路径,Currently failed 大于 0 说明过滤器正在匹配新的攻击日志。
验证过滤器匹配
如果想确认过滤器能匹配现有日志(不实际封禁),用 fail2ban-regex:
# 测试 SASL 认证失败匹配
sudo fail2ban-regex /home/nite/pod/docker-mailserver/logs/mail.log "postfix[mode=auth]"输出示例:
Failregex: 586 total
|- #) [# of hits] regular expression
| 1) [586] ^[^[]*\[<HOST>\](?::\d+)?: SASL ((?i)LOGIN|PLAIN|(?:CRAM|DIGEST)-MD5) authentication failed:...
Lines: 58500 lines, 0 ignored, 586 matched, 57914 missed586 条匹配说明过滤器正确识别了 SASL 暴力破解日志。
UFW 兼容性
服务器如果装了 UFW 防火墙,fail2ban 的 iptables-allports action 仍然正常工作。fail2ban 在 INPUT 链头部插入跳转规则:
Chain INPUT (policy DROP)
num target prot opt source destination
1 ts-input all -- 0.0.0.0/0 0.0.0.0/0 # Tailscale
2 f2b-postfix-sasl tcp -- 0.0.0.0/0 0.0.0.0/0 # fail2ban(自动插入)
3 ufw-before-logging-input ...fail2ban 的规则在 UFW 规则之前执行,被封 IP 的请求在到达 UFW 之前就被 REJECT 了,不影响现有防火墙逻辑。
确认封禁生效
等待一段时间后(通常几分钟内就有 IP 达到 5 次阈值),检查:
sudo fail2ban-client status postfix-saslStatus for the jail: postfix-sasl
|- Filter
| |- Currently failed: 8
| |- Total failed: 47
| `- File list: /home/nite/pod/docker-mailserver/logs/mail.log
`- Actions
|- Currently banned: 3
|- Total banned: 12
`- Banned IP list: 211.228.97.97 189.52.52.162 182.139.39.150Banned IP list 出现 IP 就说明封禁已生效。查看 fail2ban 日志确认:
sudo tail /var/log/fail2ban.log... [postfix-sasl] Found 211.228.97.97 - 2026-07-09 07:01:40
... [postfix-sasl] Found 211.247.127.250 - 2026-07-09 07:01:57
... [postfix-sasl] Found 189.52.52.162 - 2026-07-09 07:02:10
... [postfix-sasl] Ban 211.228.97.97查看 iptables 中的封禁链:
sudo iptables -L f2b-postfix-sasl -nChain f2b-postfix-sasl (1 references)
target prot opt source destination
REJECT all -- 211.228.97.97 0.0.0.0/0 reject-with icmp-port-unreachable
REJECT all -- 189.52.52.162 0.0.0.0/0 reject-with icmp-port-unreachable参考
- fail2ban GitHub — 官方文档与 changelog
- fail2ban postfix-sasl filter not working, use postfix auth mode instead —
postfix-sasl被postfix[mode=auth]替代的原因 - ArchWiki: Fail2ban — Arch 上的配置参考
- jail.conf(5) man page — 配置文件加载顺序说明