Firewalling and brute force mitigation

So many bad guys everywhere. Paranoia you say? Well, take a look at your mail log. While I was preparing this guide I could barely follow it. The mail server I used for testing was by far not ready and I already got visits by a bunch of foreign IP addresses trying to guess accounts of email users. Scoundrels constantly scan the internet for mail servers and do not tire to find a way to abuse them. Let’s do something about that before they actually guess email accounts successfully.

First install the nftables package:

apt install nftables

Basic firewall rules with nftables

Let’s begin by blocking all incoming network ports except those that are needed to run your mail server. Previous versions of Debian used the iptables tool to help you add firewall rules. Debian Buster comes with a newer version of the Linux kernel that is moving from iptables to nftables. The good news is that you can still use iptables commands. The bad news is that most tools do not yet support nftables. However one distant future day the old syntax will not work any more so let’s dive straight into the new syntax and use nftables.

First let’s make the iptables command use nftables. Run:

update-alternatives --config iptables

You will be able to choose between iptables-nft (the new stuff) and iptables-legacy (the old stuff). Go with iptables-nft. Now the iptables command will manage nftables rules. However you can explicitly use the iptables-legacy command if you want to access the old-school iptables rules.

So where would you store the firewall rules? The recommended way is to create a file /etc/nftables.conf and add rules there. Although the new syntax takes some getting used to, it is at least pretty readable. This is my default file for new mail servers:

#!/usr/sbin/nft -f
flush ruleset
table inet filter {
  chain input {
    type filter hook input priority 0; policy drop;

    iifname lo accept
    ct state established,related accept
    tcp dport { ssh, http, https, imap2, imaps, pop3, pop3s, submission, smtp } ct state new accept

    # ICMP: errors, pings
    ip protocol icmp icmp type { echo-request, echo-reply, destination-unreachable, time-exceeded, parameter-problem, router-solicitation, router-advertisement } accept
    # ICMPv6: errors, pings, routing
    ip6 nexthdr icmpv6 counter accept comment "accept all ICMP types"

    # Reject other packets
    ip protocol tcp reject with tcp reset

Bear with me that I will not explain this file in-depth. Feel free to read more about nftables in the Debian wiki and the wiki of the nftables project.

To active these firewall rules use systemctl:

systemctl enable nftables
systemctl start nftables

If all worked as expected you can see that systemd loaded your rules:

systemctl status nftables

You should see “Active: active (exited)” as the status. It is normal that its status is “exited” because there is no permanent process running. The rules are applied just once and that’s it.

Although nftables has been there for quite a while many projects still do not support it. Fortunately an important tool to block off attackers does: fail2ban. The “#include” line shown in the file above refers to that.

Brute-force attacks

Let’s install fail2ban:

apt install fail2ban

This is a pretty little tool. It knows about a lot of patterns in your log files that show something harmful. The software runs in the background and follows the various log files. Each new line in the log is compared against regular expressions. If a match is found it blocks the IP address that was mentioned in the log. However by default fail2ban uses the deprecated iptables tool. Let’s make it use nftables, too.

Once again edit your /etc/nftables.conf file and at the end add this line:

include "/etc/nftables/fail2ban.conf"

Create that /etc/nftables directory…

mkdir /etc/nftables

…and in there create the fail2ban.conf file containing these lines:

#!/usr/sbin/nft -f

table inet fail2ban {
  chain input {
    type filter hook input priority 100;

Why not ‘table ip’?

As the firewall just needs to block IP addresses you might be tempted to use ‘table ip’ instead of ‘table inet’. However ‘ip’ only deals with IPv4 packets. You would have to manage ‘ip’ and ‘ipv6’ tables. So I am just going with ‘table inet’ here which is more versatile.

Restart nftables

systemctl restart nftables

…and you should see a new table called fail2ban if you run:

nft list tables

Very nice. fail2ban just needs a little fine-tuning to use that table. Create a new file /etc/fail2ban/action.d/nftables-common.local that overrides the settings in the nftables-common.conf file and write:

nftables_table = fail2ban
blocktype = drop

What it means…

  • nftables_table = fail2ban
    (That is the name of the nftables table you just created. There fail2ban will add IP addresses to be blocked.)
  • blocktype = drop
    (This way we just ignore attackers. Packets from them are thrown away instead of answered with an ICMP response.)

Create another file /etc/fail2ban/jail.local with…

banaction = nftables-multiport
chain     = input

If you are curious and would like to get informed about every attacker that was blocked you may add “action = %(action_mwl)s” that also gets WHOIS information about the attacker. However this gets boring quickly and often WHOIS information is censored anyway – especially from organisations that are home to spammers.

By default an IP address gets blocked for 10 minutes (bantime) if more than 5 (maxretry) failures happened within 10 minutes (findtime). Check the jail.conf file for more information and feel free to add your preferred values to your jail.local file.

Also by default only failed login attempts using SSH are detected. There are lots of filters for other events and you just need to enable them. This once again goes into your jail.local file. I suggest you add these lines:

enabled = true

enabled = true
port    = pop3,pop3s,imap2,imaps,submission,465,sieve

enabled = true

Why the port definition for [dovecot]?

You may wonder why in the example above I suggest defining port in the dovecot section. The reason is Debian bug #867374. The default configuration calls the service “imap” instead of “imap2” and that leads to an error because there is no “imap” service defined in /etc/services.

After all these changes it is a good idea to check if your fail2ban configuration is still valid:

fail2ban-server -t

Is fail2ban happy? Great. Restart fail2ban after your last change:

systemctl restart fail2ban

To see what fail2ban did take a look at /var/log/fail2ban.log. This is what it looked like after a few minutes:

fail2ban.filter  : INFO    [sshd] Found - 2019-12-27 20:50:43
 fail2ban.filter  : INFO    [sshd] Found - 2019-12-27 20:50:43
 fail2ban.filter  : INFO    [sshd] Found - 2019-12-27 20:50:44
 fail2ban.filter  : INFO    [sshd] Found - 2019-12-27 20:50:46
 fail2ban.filter  : INFO    [sshd] Found - 2019-12-27 20:50:46
 fail2ban.filter  : INFO    [sshd] Found - 2019-12-27 20:50:48
 fail2ban.filter  : INFO    [sshd] Found - 2019-12-27 20:50:49
 fail2ban.filter  : INFO    [sshd] Found - 2019-12-27 20:50:50
 fail2ban.filter  : INFO    [sshd] Found - 2019-12-27 20:50:54
 fail2ban.filter  : INFO    [dovecot] Found - 2019-12-27 20:51:09
 fail2ban.filter  : INFO    [dovecot] Found - 2019-12-27 20:51:29
 fail2ban.filter  : INFO    [sshd] Found - 2019-12-27 20:51:33
 fail2ban.filter  : INFO    [sshd] Found - 2019-12-27 20:51:35
 fail2ban.filter  : INFO    [sshd] Found - 2019-12-27 20:51:35
 fail2ban.actions : NOTICE  [sshd] Ban
 fail2ban.filter  : INFO    [sshd] Found - 2019-12-27 20:51:37
 fail2ban.filter  : INFO    [dovecot] Found - 2019-12-27 20:51:42
 fail2ban.filter  : INFO    [dovecot] Found - 2019-12-27 20:52:03
 fail2ban.filter  : INFO    [dovecot] Found - 2019-12-27 20:52:18
 fail2ban.actions : NOTICE  [dovecot] Ban

Status please

fail2ban comes with a handy shell command called fail2ban-client. To see which jails (=where to look for attackers) are active:

$> fail2ban-client status
|- Number of jail:    4
`- Jail list:    apache-auth, dovecot, postfix, sshd

So all four expected jails are active. Do you want to know which IP addresses are banned for failed SSH login attempts?

$> fail2ban-client status sshd
Status for the jail: sshd
|- Filter
|  |- Currently failed:    3
|  |- Total failed:    13
|  - File list:    /var/log/auth.log - Actions
   |- Currently banned:    4
   |- Total banned:    5
   `- Banned IP list: 2a03:4000:1c:3f0::1

Yes, it works for IPv6, too. 🙂

Banning magic

Do you wonder how that banning actually works? fail2ban just adds new rules to your host’s firewall. Take a look at the fail2ban table you created earlier:

$> nft list table inet fail2ban
table inet fail2ban {
  set f2b-sshd {
    type ipv4_addr
    elements = {,, }

  set f2b-dovecot {
    type ipv4_addr

  set f2b-sshd6 {
    type ipv6_addr
    elements = { 2a03:4000:1c:3f0::1 }

  chain input { … }

nftables is pretty efficient with handling multiple IP addresses. It uses sets which are basically lists of IP addresses. iptables used to have the ipset functionality for that purpose. You can see in the output that it uses sets it names “elements”.

Look at the current status of a jail:

fail2ban-client status postfix-ispmail

It will show something like:

Status for the jail: postfix-ispmail
|- Filter
| |- Currently failed: 1
| |- Total failed: 168
| `- File list: /var/log/mail.log
`- Actions
 |- Currently banned: 14
 |- Total banned: 36
 `- Banned IP list: …

Grab a bag of popcorn and enjoy how your mail server grows its list of blocked idiots. 🙂

22 thoughts on “Firewalling and brute force mitigation”

  1. Florian Effenberger

    It’s not clear what “and you should see a new table called fail2ban like this” means – how to see the table? 🙂

    For fail2ban, there is no “apache” ruleset. The configuration listed yields

    Found no accessible config files for ‘filter.d/apache’ under /etc/fail2ban
    Unable to read the filter ‘apache’
    Errors in jail ‘apache’.
    ERROR: test configuration failed

    Looking into /etc/fail2ban/jail.conf shows several Apache-related configurations, like apache-auth (which seems to be referred later on this page too).

    1. Christoph Haas

      Indeed. Fixed. And suprisingly on my test system I used “apache-auth”. Must have been a copy/paste error.

  2. Azazel Mendoza

    sorry, for me the nftables is completely new, someone can help me, after installing and configuring it closes my ssh port I can not enter, likewise close my bind9 dns ports since on that server I have the dns.

    Where do I add those rules? can you help me, please

  3. Hi Christoph,

    yeah, almost through your tutorial — thanks a lot at this point 🙂
    I noticed the listing of chain f2b-postfix-ispmail, which reminds me of iptables syntax. Since nftables are used in this tutorial, might these lines be a leftover of a previous version?


    1. Christoph Haas

      Hi Gabe. I hope you get through it this weekend. It feels good once everything is set up and running. 🙂
      Thanks for the hint of the iptables output. I have removed it.

  4. Just found a little oddity: I had to enable auth_verbose = yes in 10-logging.conf, otherwise Dovecot was not logging authentication failures, and not playing nicely with fail2ban… 😉

  5. Hi, I think we’re missing a rule for allowing managesieve port.
    I enabled it with a `tcp dport sieve ct state new accept` line in /etc/nftables.conf

  6. This:

    tcp dport ssh ct state new accept
    tcp dport http ct state new accept
    tcp dport https ct state new accept
    tcp dport imap2 ct state new accept
    tcp dport imaps ct state new accept
    tcp dport pop3 ct state new accept
    tcp dport pop3s ct state new accept
    tcp dport submission ct state new accept
    tcp dport smtp ct state new accept

    Can be replaced with a single line:

    tcp dport { ssh, http, https, imap2, imaps, pop3, pop3s, submission, smtp } ct state new accept

  7. Being unfamiliar with nftables I could use a little help figuring out a problem. My ISP blocks port 25 so I use dynu to forward incoming mail to port 2525. Everything was working fine until I enabled your default rules, then the mail server became unreachable by dynu.

    changes to postfix
    2525 inet n – y – – smtpd
    #smtp inet n – y – – smtpd

    Would changing SMTP to 2525 in the last rule fix it, or do I need to change something else.

    Other than that little hiccup it has been it is a great guide and no problems.

  8. Should

    be in “/etc/fail2ban/jail.d/jail.local” instead of “/etc/fail2ban/jail.local”? Reason I ask is because on top of that “/etc/fail2ban/jail.local” file there is a banner that states.

    # It will probably be overwritten or improved in a distribution update.
    # Provide customizations in a jail.local file or a jail.d/customisation.local.

  9. For that last post this is what I meant to add.

    banaction = nftables-multiport
    chain = input

    enabled = true

    enabled = true
    port = pop3,pop3s,imap2,imaps,submission,465,sieve

    enabled = true

  10. Daniél Lecoq

    You’re missing a comma between pop3 and pop3s in the /etc/nftables.conf example. I noticed when I was lazy and copied it. But the systemctl status nftables said where the error was, so easy to fix.

    1. Christoph Haas

      Very sorry. I thought that I’d optimize the config and was too lazy to copy it from the test system.

  11. DId I miss something? Wasn’t the postfix setting in jail.local just postfix and not postfix-ispmail?
    Wouldn’t the command “$fail2ban-client status postfix-ispmail” be an error in the instructions? Shouldn’t be just “$fail2ban-client status postfix”

  12. Hi there,
    First of all, your guide is awesome!

    I was setting up nft on another server and as I barely know anything about nft, I used your template.
    When I was typing the “ip protocol tcp reject with tcp reset” line, I just thought that i should probably block udp packets without an allow rule too, but “ip protocol udp reject with udp reset” dosent work.

    Any way to block udp? Or am I just stupid?

Leave a Reply

Your email address will not be published. Required fields are marked *

Scroll to Top