Configuring Fail2Ban for a Podman Mail Server With UFW Bans
My self-hosted mail server, running Postfix and Dovecot, lives in a rootless Podman container on a netcup VPS. A Postfix log summary showed 2,477 rejected messages in one day, with 2,471 coming from the same IP. There were also 152 failed SASL LOGIN attempts, along with scanning, relay abuse, and TLS probes.
Postfix can reject spam itself, but SASL password guessing is a separate problem. Fail2Ban identifies sources with repeated failures in the logs and tells the firewall to ban them. Since the server already uses UFW to manage inbound rules, Fail2Ban uses its built-in ufw action instead of writing directly to iptables.
Environment
| Item | Value |
|---|---|
| Host OS | Arch Linux |
| Container runtime | Rootless Podman |
| Mail server image | ghcr.io/docker-mailserver/docker-mailserver:latest |
| Mail server service | systemd-mailserver |
| Quadlet file | ~/pod/docker-mailserver/mailserver.container |
| Host mail log | ~/pod/docker-mailserver/logs/mail.log |
| Container mail log | /var/log/mail/mail.log |
| Fail2Ban | 1.1.0 |
| Firewall | UFW |
1. Install and inspect the current state
Install and start Fail2Ban:
sudo pacman -S fail2ban
sudo systemctl enable --now fail2banConfirm that UFW is enabled, Fail2Ban is running, and see which jails are active:
sudo ufw status verbose
sudo systemctl is-active fail2ban
sudo fail2ban-client statusUFW should report Status: active. Once this configuration is complete, the jail list includes:
Status
|- Number of jail: 3
`- Jail list: postfix, postfix-sasl, sshdbackend and banaction are separate settings:
backenddetermines where Fail2Ban reads failure records, such as journald or a regular log file.banactiondetermines how an IP is banned once it reaches the threshold.
The mail container writes logs to a host-mounted file, so the mail jails need backend = polling. Switching to UFW does not change log collection.
2. Make the Postfix jails read the container log
A traditional Postfix installation usually writes logs to /var/log/mail.log. This path inside the container is not visible to Fail2Ban on the host, so use the actual host-side volume path instead.
Fail2Ban on Arch selects the systemd log backend for Postfix through paths-arch.conf by default. The logs here are files, so explicitly configure polling in a local override.
Create or edit /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 = ufw
[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 = ufwUse the .local suffix. Fail2Ban reads jail.conf, jail.d/*.conf, jail.local, and jail.d/*.local in order, so this local file overrides the defaults last.
3. Change the sshd banning action to UFW
Do not edit /etc/fail2ban/jail.conf; it is package-provided and may be overwritten during an update.
To make UFW the default action for jails, edit /etc/fail2ban/jail.local and set this in [DEFAULT]:
[DEFAULT]
banaction = ufw
banaction_allports = ufwTo switch only the SSH jail, use this instead:
[sshd]
banaction = ufwPostfix and postfix-sasl already set banaction = ufw individually in mailserver.local, so they do not depend on the global setting.
Fail2Ban’s built-in UFW action calls ufw to add and remove rules. There is no longer a need to inspect Fail2Ban ban chains with iptables -L; check UFW directly instead.
4. Validate the configuration, then restart the jails
Parse the configuration after editing:
sudo fail2ban-client -d >/dev/nullThis command only checks the parsed result; it does not modify the firewall.
After changing an action, restart the service completely:
sudo systemctl restart fail2ban5. Verify logs, actions, and UFW rules
Confirm that the mail jails are reading the host log file:
sudo fail2ban-client status postfix-sasl
sudo fail2ban-client status postfixThe File list output should be:
/home/nite/pod/docker-mailserver/logs/mail.logTo test whether the SASL filter matches existing log entries without issuing a ban:
sudo fail2ban-regex /home/nite/pod/docker-mailserver/logs/mail.log "postfix[mode=auth]"After restarting the service, inspect the runtime actions instead of relying only on the configuration files:
sudo systemctl is-active fail2ban
sudo fail2ban-client get sshd actions
sudo fail2ban-client get postfix actions
sudo fail2ban-client get postfix-sasl actionsThe actual output in this case was:
active
The jail sshd has the following actions:
ufw
The jail postfix has the following actions:
ufw
The jail postfix-sasl has the following actions:
ufw6. Run an end-to-end test
sudo fail2ban-client set sshd banip 192.0.2.1
sudo ufw status numbered | grep -F 192.0.2.1
sudo fail2ban-client set sshd unbanip 192.0.2.1The second command should show a UFW rule with a by Fail2Ban comment. The third command removes the test ban immediately.