给 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 容器环境中,有两个选择:

  1. journald 后端:如果容器日志通过 journald 输出,fail2ban 可以用 backend = systemd 从 journald 读取
  2. polling 后端:如果容器把日志写到挂载的文件中,fail2ban 用 backend = polling 直接读文件

本文的邮局容器通过 Volume 挂载把日志写到了宿主机的 ~/pod/docker-mailserver/logs/mail.log,所以选 polling 后端。

注意:Arch Linux 的 fail2ban 默认通过 paths-arch.confpostfix_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-sasl vs postfix[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-sasl
Status 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 missed

586 条匹配说明过滤器正确识别了 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-sasl
Status 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.150

Banned 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 -n
Chain 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

参考