Configuring Fail2Ban for a Podman Mail Server With UFW Bans

Contents

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 fail2ban

Confirm 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 status

UFW should report Status: active. Once this configuration is complete, the jail list includes:

Status
|- Number of jail:  3
`- Jail list:       postfix, postfix-sasl, sshd

backend and banaction are separate settings:

  • backend determines where Fail2Ban reads failure records, such as journald or a regular log file.
  • banaction determines 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 = ufw

Use 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 = ufw

To switch only the SSH jail, use this instead:

[sshd]
banaction = ufw

Postfix 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/null

This command only checks the parsed result; it does not modify the firewall.

After changing an action, restart the service completely:

sudo systemctl restart fail2ban

5. 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 postfix

The File list output should be:

/home/nite/pod/docker-mailserver/logs/mail.log

To 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 actions

The 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:
ufw

6. 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.1

The second command should show a UFW rule with a by Fail2Ban comment. The third command removes the test ban immediately.

References

Contents