Banning Malicious Hosts Automatically with Fail2Ban on Ubuntu
August 10, 2025
If you're getting unwanted requests to your server, you can use Fail2Ban to automatically set firewall rules to reject new connections from those hosts. Some people use it to limit SSH brute force attacks. I've heard the argument that Fail2Ban isn't necessary if you're using public key authentication, however, it's more useful than that. In this post, I'm going to cover the installation, some SSH daemon jail configuration, but I'm also going to show you how to write your own expression to match malicious HTTP requests, and then ban the corresponding host IP address.
The fail2ban package comes with a number of binaries. One of them is fail2ban-server. The server monitors log files and bans IP addresses by setting firewall rules. Another one of the binaries is the fail2ban-client. The client reads configuration files and is used to configure the server.
There's a concept of jails. You configure a jail, which makes use of filters and actions. While configuring the jail, you specify what log or logs to monitor, how long to ban for, and more. The filter is what's used to match what you're looking for in log files. When there's a match, some action is taken such as banning an IP address. The package comes with filters and actions, but you can also create your own.
Let's begin by updating the package lists and installing the fail2ban package.
sudo apt update && sudo apt install fail2ban -y
A unit file will also be created, and the service will be enabled, so fail2ban will start automatically on boot. Once that's done, change directories to /etc/fail2ban. This is where the configuration files are.
At the bottom of /etc/fail2ban/jail.d/defaults-debian.conf there is:
[sshd] enabled = true
which means the sshd jail is enabled by default. The sshd jail is for SSH authentication failures.
We can use fail2ban-client to confirm that the sshd jail is recognized. Try running:
sudo fail2ban-client status
The output:
Status |- Number of jail: 1 `- Jail list: sshd
You can run man fail2ban-client to learn more about how it can be used.
All enabled jails will show in that jail list, as long as their configurations are known of. If we were to add an additional one, we'd have to reload the configuration.
It's helpful that fail2ban comes with an SSH jail configured, because hosts are going to be banned for authentication failures as soon as we install the package. We may want to adjust the configuration some though, and I'll cover that soon.
In addition to the sshd jail, there are other jail defaults in place that aren't enabled. Those can be found in /etc/fail2ban/jail.conf. In that same file, there are global defaults as well.
Run view +100 /etc/fail2ban/jail.conf and you'll see these just below your cursor.
# "bantime" is the number of seconds that a # host is banned. bantime = 10m # A host is banned if it has generated # "maxretry" during the last "findtime" # seconds. findtime = 10m # "maxretry" is the number of failures # before a host get banned. maxretry = 5
This means that by default, if there are 5 failures within 10 minutes, then a host is going to be banned for 10 minutes. These are default settings that apply to every jail unless they're overridden. The sshd jail doesn't come configured with these values, so the default ones we see here apply.
Type :q and then hit the enter key to exit.
Personally, I override the sshd jail configuration with my own, mostly because I want to ban for longer. One way to do that is to create a configuration file within jail.d. Granted, if the SSH port is changed, log activity slows significantly, because connection attempts over port 22 are refused. That's not feasible for everyone though, so here's an example of how you could modify the sshd jail.
Using sudo, create and begin to edit /etc/fail2ban/jail.d/sshd.local
The files and directories within /etc/fail2ban are owned by root, so if you're not root, then you'll have to use sudo to create files.
The settings below mean that if there's even one failed attempt, then we're banning the host for a week. This is fine if your server is getting a relatively low amount of attempts, but if you find that your sshd jail is accumulating many IPs, then it'd be a good idea to ban the repeat offenders for a longer period of time than the one-offs. I'm going to show you how to do that when we make our custom jail later in this post.
[sshd] enabled = true maxretry = 1 bantime = 1w
The sshd jail is already enabled in a separate config file, but I like to organize myself by including it here rather than there. The file I'm talking about is /etc/fail2ban/jail.d/defaults-debian.conf
You can leave that file alone, it's not going to hurt by being enabled in two spots. I just like to enable the jail in a file I maintain for good measure.
Setting maxretry to 1 may seem risky, because you could ban yourself if you make one mistake, but if you use an SSH config file with a key pair, then the likelihood of that happening is low. I don't remember the last time I banned myself. Regardless, if it were to happen, you should be able to access the server via the console. There's the option to whitelist yourself, but your home IP address will change at some point. I explain how to configure an SSH config file here https://charlesbeadle.tech/posts/ssh#config-file.
If you did want to allow for more than one failure, here's an example of how you could do that:
[sshd] enabled = true maxretry = 3 findtime = 1h bantime = 1w
Here we're saying that if there are three failures within one hour, then we want to ban for a week.
Lastly, for this jail, I'm going to include the port setting. If you were to change your SSH port to say 8522, then you'd include it like so:
[sshd] enabled = true maxretry = 3 findtime = 1h bantime = 1w port = 8522
This causes requests from this host over port 8522 to be rejected. It's also possible to ban the host from all ports.
Once you settle on a configuration, go ahead and save/exit the file. We're going to have to reload our configuration for these changes to take effect. You can do that by running:
sudo fail2ban-client reload sshd
You may see a warning that says:
WARNING 'allowipv6' not defined in 'Definition'. Using default one: 'auto'
We'll come back to that. We've reloaded the configuration, but we can also confirm that our changes are recognized with:
sudo fail2ban-client get sshd maxretry
Is the output consistent with what was set? If so, great. You can do that with other settings as well. For example:
sudo fail2ban-client get sshd bantime
There is an order in which configuration files are parsed. Recall in /etc/fail2ban there was a jail.conf file. Then, we also have the jail.d directory. The .conf files in jail.d override jail.conf, because they are parsed after jail.conf. The .local files are parsed after the .conf files, so if there were a /etc/fail2ban/jail.d/sshd.local file and a /etc/fail2ban/jail.d/sshd.conf file, then the .local file would override the .conf file. More details can be found on this in the "CONFIGURATION FILES FORMAT" section of the jail.conf man page. man jail.conf. If a configuration change is made and the changes aren't in effect after reloading the configuration, then this could be the cause.
Going back now to that warning (if you had it) that read "WARNING 'allowipv6' not defined in 'Definition'. Using default one: 'auto'".
We can resolve that by creating the /etc/fail2ban/fail2ban.local file and including the following:
[DEFAULT] allowipv6 = auto
Then, reload with sudo fail2ban-client reload, which reloads all jails, and notice that we don't get warned this time.
Aside from SSH, if there's something else that you'd like fail2ban to monitor, then you can create a configuration for that as well.
For example, if you're running a web server, then you may have a /var/log/nginx/access.log or /var/log/apache2/access.log file or something similar. If you look at the access log, you may see a bunch of requests for env files and other files that nobody or nothing should be trying to read.
We can provide fail2ban with a regex that'll be used in a filter to match these requests and ban the respective hosts. Before writing our filter, let's take a look at a couple of examples of requests for an env file found in an nginx access log.
192.0.2.92 - - [09/Aug/2025:03:00:29 -0400] "GET /.env HTTP/1.1" 404 196 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"
192.0.2.92 - - [09/Aug/2025:06:07:53 -0400] "GET /.env HTTP/1.1" 404 134 "-" "Mozilla/5.0 (Linux; U; Android 4.4.2; en-US; HM NOTE 1W Build/KOT49H) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 UCBrowser/11.0.5.850 U3/0.8.0 Mobile Safari/534.30"
We can prevent further requests from this host for as long as we choose by defining a custom filter and jail. A filter is something that works as a check that says hey fail2ban, if you see this, then I want the corresponding host banned. In order to define the filter, examine these two samples. There's a pattern. You have the IP address, a couple of dashes, the date, and then a quote followed by GET and then the path that includes .env.
Within /etc/fail2ban/filter.d let's create a file. We could call it envfile.conf since we want to ban those trying to read env files. I don't want to get too in the weeds here explaining regular expressions since this is a fail2ban post, but I will at least explain the expression. Here's an example of one that I've used to match requests for .env files:
[Definition] failregex = ^<ADDR> - - \[\] "GET /\.env\s
The filter is defined within a Definition section [Definition]
Both of the log samples started with the IP address, so we indicate that the beginning of the line should start with <ADDR> which is a fail2ban tag that stands for the IP address. An access log is bound to have more writes than the SSH portion of the systemd journal, especially if an alternate port is being used for SSH. For an access log, it's good practice to avoid wildcards like .*. This is because there's a lot of log activity, and fail2ban is constantly monitoring the log and testing this pattern against each line. The more explicit the expression the more efficiently this process goes. After <ADDR> we're using the exact characters that are present in the log. The brackets are special characters, so they're escaped along with the dot character in .env. Now, between the brackets, there's nothing in place of the date. This is because fail2ban recognizes the date pattern and strips it out before the failregex search begins. The failregex is the regex we're writing. For clarity, one of those log samples we looked at was:
192.0.2.92 - - [09/Aug/2025:03:00:29 -0400] "GET /.env HTTP/1.1" 404 196 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"
This line would become:
192.0.2.92 - - [] "GET /.env HTTP/1.1" 404 196 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"
So we just have to escape the brackets and not worry about the date. Finally, the regex ends with a whitespace character \s because it's specifically intended to match requests for files named .env. We don't have to concern ourselves with the rest of the line. This is sufficient to get a match and ban a host.
Some things to mentally note. Attempts to read env files will continue, but at least those that try will get banned. An env file shouldn't be in a public folder to begin with. Files such as these are often named .env.production, production.env, .env.dev, and not just .env. I've noticed though, that hosts trying to do this try a variety of file names, and .env is virtually always included. The point is that these are malicious hosts, and personally, I don't want them poking around. Plus, this gives us an example of how we could create a filter.
We can test out our failregex using a program called fail2ban-regex that comes with fail2ban. If you just installed nginx though, then there may not be a line in the access log that will match.
Here are some usage examples:
fail2ban-regex /var/log/nginx/access.log '^<ADDR> - - \[\] "GET /\.env\s'
When you're tuning your regex, then passing it directly this way allows you to quickly iterate on it.
Another way to do this is to pass the filter name, which will reference /etc/fail2ban/filter.d/envfile.conf
fail2ban-regex /var/log/nginx/access.log envfile
For me, part of the output from fail2ban-regex read:
Results ======= Failregex: 27 total |- #) [# of hits] regular expression | 1) [27] ^<ADDR> - - \[\] "GET /\.env\s
Lines: 6400 lines, 0 ignored, 27 matched, 6373 missed [processed in 0.58 sec]
The output shows 27 hits. If I wanted to see the lines that matched, I could have used the --print-all-matched option.
fail2ban-regex --print-all-matched /var/log/nginx/access.log '^<ADDR> - - \[\] "GET /\.env\s'
There's also a --print-all-missed option, which can be useful to see if there are lines not matching that you would like to match. You could combine this with grep to reduce the output.
Recall that the amount of lines that didn't match was over 6,000. If we were to narrow the results to lines that include .env in them using grep, and then count the lines using the wc command, we'd have far fewer.
fail2ban-regex --print-all-missed /var/log/nginx/access.log '^<ADDR> - - \[\] "GET /\.env\s' | grep .env | wc -l
54
If you still get a decent amount of lines, then consider piping to less so you can page down through the lines.
fail2ban-regex --print-all-missed /var/log/nginx/access.log '^<ADDR> - - \[\] "GET /\.env\s' | grep .env | less
That does it for our filter. I suggest checking out this page on filter security as well. https://fail2ban.readthedocs.io/en/latest/filters.html#filter-security
Next, we configure the jail. In the jail, we indicate which log files we want monitored, what filter to use, how many failures are acceptable, how long to ban for, etc.
To configure a jail, or a number of jails, you could create a jail.local file in /etc/fail2ban and maintain all of them there, or, you could maintain them all separately under jail.d. Personally, I keep them separate for portability reasons. To configure this envfile jail, I'm going to create /etc/fail2ban/jail.d/envfile.local and enter the following:
[envfile] enabled = true backend = auto usedns = no filter = envfile logpath = /var/log/nginx/access.log maxretry = 1 bantime = 3h bantime.increment = true bantime.factor = 1 bantime.multipliers = 1 2 4 8 16 32 64 280 bantime.rndtime = 6h bantime.maxtime = 846h
The jail consists of a section [envfile] where we specify the jail name, and options that we set. Some options like enabled are self explanatory, but by default, fail2ban comes configured with backend set to systemd. We've set logpath to the access log though, so it's not the systemd journal that we want to monitor. Instead, we've set backend to auto. Setting usedns to no means that we don't want to use hostnames for banning. We used the <ADDR> tag in the regex rather than <HOST> which is for both IP addresses and hostnames if usedns is enabled. For filter, notice that we didn't have to include the file extension. Then, we're instructing fail2ban to immediately ban for 3 hours when a host makes a request that matches our failregex. When we do ban, we're using incremental ban time. Some offenders are worse than others. This way we can ban repeat offenders for increasingly long periods of time while releasing those that tried maybe once. This helps keep our amount of banned hosts down which is easier on the system. You can read more about incremental ban time in /etc/fail2ban/jail.conf. Search for bantime.increment.
Now that we have both our filter and jail configured, we need to reload the configuration. We're not able to run sudo fail2ban-client reload envfile, because the jail isn't recognized yet. Instead, we can run sudo fail2ban-client reload. Now, if we run sudo fail2ban-client status, the new envfile jail should show up in our jail list.
After some time, you can try checking the status of the new envfile jail with sudo fail2ban-client status envfile which provides a banned IP list along with other useful information.
If it's just the list of IP addresses that you're after, then you can use:
sudo fail2ban-client get envfile banned
As you can see, Fail2Ban is a powerful and flexible tool. One of the main points that I wanted to drive home here was that Fail2Ban isn't just for SSH authentication failures. We used the example of an access log, but you can use this tool to monitor any log file. No single tool is a silver bullet, however, Fail2Ban is effective. Combining this with other strategies will serve you well. We covered a decent amount here, plenty to get started, but there's much more. A Fail2Ban wiki is maintained at https://github.com/fail2ban/fail2ban/wiki. Remember there was also man jail.conf which was helpful, plus numerous notes in the /etc/fail2ban/jail.conf file.
If you read this far, thanks for taking the time. There's a whole lot that could be covered on this subject, so if there's something I haven't explained thoroughly enough, then feel free to either comment below, or send me an email at contact@charlesbeadle.tech.
If this was helpful to you and you'd like to show your appreciation, feel free to buy me a coffee. Every little bit helps, and your support means a lot! Thanks!