I have correctly installed fail2ban in my machine, activating the rules for ssh, ssh-dos and recidive; it all works ok.

Lately, I have seen an increasing patterns of repetitive attacks from different hosts form the same networks, which circumvent the "recidive" rule by switching IP after a ban:

2015-01-25 11:12:11,976 fail2ban.actions: WARNING [ssh] Ban XXX.41.124.29
2015-01-25 11:12:13,165 fail2ban.actions: WARNING [ssh] Ban XXX.41.124.42
2015-01-25 11:12:16,297 fail2ban.actions: WARNING [ssh] Ban XXX.41.124.28
2015-01-25 11:12:20,446 fail2ban.actions: WARNING [ssh] Ban XXX.41.124.104

I would like to detect it and make a "recidive24" rule that blocks all these kind of attacks banning the whole /24 block.

I found a suggestion in the debian bug archive for fail2ban, and I have applied it, but:

  1. If I apply the full /24 ban when the ssh jail is triggered, I have the problem that it is easy from someone on my same network to block me out, by just attacking from ONE IP;

  2. The recidive jail would be perfect, but it is not triggered by the storm changing IPs...

So I would like to change the recidive filter specification so that it just look at the first three bytes of the IP, but I am at a loss here... the regexp that do the ban is (from /etc/fail2ban/recidive.conf) is

# The name of the jail that this filter is used for. In jail.conf, name the 
# jail using this filter 'recidive', or change this line!
_jailname = recidive

failregex = ^(%(__prefix_line)s|,\d{3} fail2ban.actions:\s+)WARNING\s+\[(?!%(_jailname)s\])(?:.*)\]\s+Ban\s+<HOST>\s*$

...and it will match a complete IP.

The question: How can I change this failregex so that it matches just the first three bytes of the host IP?

Please notice that a problem is not blocking the whole subnet when the spamming IP is detected --- this is relatively easy. The problem is triggering a kind of subnet-recidive when there are, for example, five or more recidive hits for the same subnetwork...

I though about filtering the fail2ban log file with another daemon and writing a second file where the last byte is 0 every time, and trigger the recidive jail using it, but it seems really clumsy...

6 Answers 6


The easiest way to add this magic /24 (some might like even /22) to the iptables commands for all jails is to add two files:


with the following contents:

actionban = <iptables> -I f2b-<name> 1 -s <ip>/24 -j <blocktype>
actionunban = <iptables> -D f2b-<name> -s <ip>/24 -j <blocktype>

For the detection part you'll need to modify filters...that's a little more tricky. But the good part is - it is not necessary! this subnet blocking stuff works quite nicely.

  • Yes, this will block all the subnet.Probably I was not so clear in the question: the real problem is to detect a storm from a subnet. Otherwise, a malicious host on my subnet will cut me out. Will re-edit...
    – Rmano
    Mar 27, 2020 at 9:46

Fail2ban doesn't have neat functionality to automatically block attacks from a whole subnet. It is possible to do, though, using a recent version of fail2ban (I use v0.11), some simple fail2ban scripts and a small, pure python3 script.

Note: The question refers to 'a whole subnet' (which I'll refer to as CIDR blocks or IP ranges). This is a difficult thing, because we don't know how large a block of addresses the attacker controls. It might even be that the attacker by chance controls a handful of addresses from the same block, with the in-between addresses being legitimate.

Step 1. Get CIDR of hosts

The log files that fail2ban monitors typically show hosts (e.g. instead of CIDR blocks ( or IP ranges ( -

A solution could be to first assume a small CIDR block and then grow it as logs report more misbehaving hosts. Obviously it should only grow the CIDR, if those hosts are from adjacent addresses. But this is complex and legitimate addresses might get caught up regardless of the sophistication of the algorithm.

Instead, we can also simply lookup the CIDR in whois. This incurs some latency for the whois lookup, and generates some traffic. But the script that parses whois, can just write the CIDR to syslog, which can then be caught by fail2ban again.

Note: don't forget to hook this script into the actionban of your preferred action.d/lorem-ipsum.conf script. Be aware that if its maxretry > 1, then you will not catch CIDR blocks where hosts only fail once!


import sys, subprocess, ipaddress, syslog

def narrowest_cidr(cidrA, cidrB):
    _ip, subA = cidrA.split('/')
    _ip, subB = cidrB.split('/')

    if int(subA) > int(subB):
        return cidrA
        return cidrB

bad_ip = sys.argv[1]
cidrs = []
inetnums = []
ret = None

whois = subprocess.run(['whois', bad_ip], text=True,
        stdout=subprocess.PIPE, check=True)

for line in whois.stdout.split('\n'):
    if 'CIDR:' in line:
        cidrs.append(line.replace('CIDR:', '').strip())
    if 'inetnum:' in line:
        inetnums.append(line.replace('inetnum:', '').strip())

if len(cidrs) >= 1:
    if len(cidrs) == 1:
        cidr = cidrs[0]
        cidr = narrowest_cidr(cidrs[0], cidrs[-1])
elif len(inetnums) > 0:
    if len(inetnums) == 1:
        inetnum = inetnums[0]
        startip, endip = inetnum.split(' - ')
        cidrs = [ipaddr for ipaddr in ipaddress.summarize_address_range(ipaddress.IPv4Address(startip), ipaddress.IPv4Address(endip))]
        if len(cidrs) == 1:
            cidr = cidrs[0]
            cidr = narrowest_cidr(cidrs[0], cidrs[-1])
    cidr = "no CIDR found"

syslog.syslog("{} has CIDR {}".format(bad_ip, cidr))

Step 2. Figure out when to block a CIDR

If we had the dynamic CIDR determination this might get a little elaborate, as we'd have to change what we're banning. But with the whois lookup we can simply ban the CIDR block we found, based on a maxretry and findtime that we deem appropriate. Here's the jail I use:

filter = fail2ban-cidr-recidive
action = nftables-common[name=BADRANGE]
logpath = /var/log/everything/current

#5 bans in 12hrs is 48hr ban
maxretry = 5
findtime = 12h
bantime = 2d

And accompanying filter


failregex = \[suspectrange\] .* has CIDR <SUBNET>

Step 3. Actually block the CIDR

As you may have noticed, I use action.d/nft-common.conf. nftables allows blocking CIDR blocks instead of single hosts. This requires a small change to the first line of the actionstart part of the action script:

actionstart = nft add set netdev f2b <set_name> \{ type ipv4_addr\; \}

Should be modified to:

actionstart = nft add set netdev f2b <set_name> \{ type ipv4_addr\; flags interval\; \}

I tried fail2ban in Centos 7 and found that it fails to block IP addresses sometimes. It keeps adding them to jail but they were still able to access sshd. There is some incompatibility with firewalld.

I now use a different approach. I modified /etc/hosts.deny like this:

sshd: 43.*.*.*
sshd: 58.*.*.*

Just don't add your own IP networks there by accident.

To grep IP addresses from the secure log, use:

grep sshd /etc/hosts.deny

To get the 10 most active IP addresses from the sshd log with access count, use:

egrep $ip /var/log/secure | sed -r "s/^.*$n($ip).*$/\1/g" | sed s/-/./g | sort | uniq -c | sort -g | tail -10

(source: http://whoishacking.com)

  • Yes, but this is manual... ;-)
    – Rmano
    Apr 22, 2015 at 20:39

fail2ban has indeed a hostsdeny.conf in /etc/fail2ban/action.d which means you only have to declare action = hostsdeny

An example from howtoforge :

# Here we use TCP-Wrappers instead of Netfilter/Iptables. 
# "ignoreregex" is used to avoid banning the user "myuser".


enabled     = true
filter      = sshd
action      = hostsdeny
              sendmail-whois[name=SSH, dest=you@mail.com]
ignoreregex = for myuser from
logpath     = /var/log/messages
  • ...but this is just banning one IP, it seems. Are you sure it is banning all the subnet? I do not see it in the definition of the action either...
    – Rmano
    Oct 20, 2016 at 9:10

This script in Python is running fine, in a crontab */5 minute, to group similar IPs into network range, from CIDR /23 up to /32; it calls fail2ban, but can call IPTABLEs directly too.


It groups this IPs



fail2ban-client set postfix-sasl banip
fail2ban-client set postfix-sasl banip
fail2ban-client set postfix-sasl banip
fail2ban-client set postfix-sasl banip

To block attacks to my apache server I choosed the following setup.

I have a filter which uses prefregex to extract the first 3 parts of an IP address and to assign this value to the fail2ban variabe ip4. For an attacker with IP address "" the variable ip4 would be set to the value "1.2.3".


before = apache-common.conf

prefregex = ^(?P<ip4>\d{1,3}\.\d{1,3}\.\d{1,3})\.\d{1,3}\s+<F-CONTENT>.*</F-CONTENT>$ 
failregex   = whatever you are looking for\b

My jail declaration then uses a modified ban action.


banaction = hackerStop-iptables-multiport

The ban action simply modifies the iptables-multiport.conf action. It expands the partial IP address written to ip4 to include ".0" as the last part of the IP address and the subnet specifier "/24".



before = iptables-multiport.conf


actionban = <iptables> -I f2b-<name> 1 -s <ip>.0/24 -j <blocktype>
actionunban = <iptables> -D f2b-<name> -s <ip>.0/24 -j <blocktype>

For example, attacks with IP address or will both be reported in the fail2ban log with the message "INFO [hackerStop] Found 64.62.197 - 2023-01-...". To unban this subnet you will have to issue the command "fail2ban-client unban 64.62.197".

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.