Configuring Fail2ban for a Podman Mail Server to Stop SASL Brute Force
Background
A self-hosted mail server (Postfix + Dovecot) runs on a netcup VPS in a Podman rootless container (the docker-mailserver image). One day, checking the Postfix log summary revealed that within 24 hours:
- 2,477 emails rejected (99%), of which 2,471 were spam from a single IP
- 152 SASL LOGIN authentication failures from over a hundred different IPs worldwide
- Shodan scans, TLS probing, relay abuse attempts, etc.
Although Postfix’s built-in anti-spam rules blocked 99% of attacks at the door—no spam was actually delivered—SASL brute force is a real threat: given a weak enough password, it is only a matter of time before it gets cracked. fail2ban automatically bans IPs at the firewall after a configurable number of failed attempts, cutting off attacks at the source.
Environment
| Item | Value |
|---|---|
| Host OS | Arch Linux |
| Container runtime | Podman (rootless, user nite) |
| Mail server image | ghcr.io/docker-mailserver/docker-mailserver:latest |
| Container name | systemd-mailserver |
| Quadlet file | ~/pod/docker-mailserver/mailserver.container |
| Mail log path (host) | ~/pod/docker-mailserver/logs/mail.log |
| Mail log path (container) | /var/log/mail/mail.log |
| fail2ban | Installed via pacman -S fail2ban, version 1.1.0 |
| Firewall | UFW (does not interfere with fail2ban) |
Key Question: Where Are the Logs?
fail2ban needs to read log files to detect attack patterns. In a traditional deployment, Postfix logs to /var/log/mail.log and fail2ban reads it directly. In a Podman container environment, there are two options:
- journald backend: If container logs go to journald, fail2ban can use
backend = systemdto read from the journal - polling backend: If the container writes logs to a mounted file, fail2ban uses
backend = pollingto read the file directly
The mail server container in this post writes logs to a volume-mounted file at ~/pod/docker-mailserver/logs/mail.log on the host, so the polling backend is the right choice.
Note: Arch Linux’s fail2ban sets
postfix_backend = systemdby default viapaths-arch.conf. If you don’t explicitly override this topollingin your jail config, fail2ban will read journald instead of the file and match nothing.
Installing fail2ban
sudo pacman -S fail2ban
sudo systemctl enable --now fail2banAfter installation, only the sshd jail is enabled by default. Confirm with sudo fail2ban-client status:
Status
|- Number of jail: 1
`- Jail list: sshdCreating the Jail Configuration
File path: /etc/fail2ban/jail.d/mailserver.local
Configuration content:
[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-allportsParameter Reference
| Parameter | Value | Description |
|---|---|---|
mode = auth |
postfix-sasl | Matches SASL authentication failures (postfix[mode=auth] replaces the old postfix-sasl filter since fail2ban 0.10+) |
mode = more |
postfix | More aggressive than default normal mode; also matches relay abuse, unknown hosts, etc. |
backend = polling |
both | Polls the log file instead of using journald |
maxretry = 5 |
5 failures within 10 minutes triggers a ban | |
findtime = 10m |
Counting window | |
bantime = 1h |
Ban duration | |
banaction = iptables-allports |
Ban method | REJECTs all ports from the banned IP |
postfix-saslvspostfix[mode=auth]: Since fail2ban 0.10.0, the standalonepostfix-saslfilter was removed and replaced with themode=authsub-mode of thepostfixfilter. In jail config, the[postfix-sasl]section still works because it internally callsfilter = postfix[mode=auth]via themode = authparameter.
Restart and Verify
# Test configuration syntax
sudo fail2ban-client -t
# Restart
sudo systemctl restart fail2ban
# List all jails
sudo fail2ban-client statusOutput should show three jails:
Status
|- Number of jail: 3
`- Jail list: postfix, postfix-sasl, sshdCheck jail details:
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 correctly points to the container log path, and Currently failed > 0 means the filter is matching new attack log entries.
Verifying Filter Matches
To confirm the filter matches existing log entries (without actually banning anything), use fail2ban-regex:
# Test SASL auth failure matching
sudo fail2ban-regex /home/nite/pod/docker-mailserver/logs/mail.log "postfix[mode=auth]"Example output:
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 matches confirm the filter correctly identifies SASL brute-force log entries.
UFW Compatibility
If the server runs UFW as its firewall, fail2ban’s iptables-allports action still works fine. fail2ban inserts a jump rule at the head of the INPUT chain:
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 (auto-inserted)
3 ufw-before-logging-input ...fail2ban’s rules execute before UFW’s rules, so banned IPs get REJECTed before reaching UFW. This does not interfere with existing firewall logic.
Confirming Bans Are Active
After some time passes (usually within minutes, an IP will hit the 5-failure threshold), check:
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.150When IPs appear in Banned IP list, the ban is active. Check the fail2ban log to confirm:
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.97Check the iptables ban chain:
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-unreachableReferences
- fail2ban GitHub — Official docs and changelog
- fail2ban postfix-sasl filter not working, use postfix auth mode instead — Why
postfix-saslwas replaced bypostfix[mode=auth] - ArchWiki: Fail2ban — Configuration reference for Arch Linux
- jail.conf(5) man page — Configuration file loading order documentation