When setting up a virtual private server (VPS) you will soon start to see lots of traffic
from bots and vulnerability scanners. See abuseipdb.com/statistics
Fail2ban is excellent tool to automatically ban these scanners. But there might not be good
filter (see /etc/fail2ban/filters.d/) available, so lets make our own filter.
Filter can have multiple failregex regex clauses which look for matching log lines.
Lets create few to ban some scanners, we need to watch for access.log and error.log
of Apache or Nginx to recognize these hostile hackers and ban them.
Here we have some example lines from error.log
2024/12/25 15:22:07 [error] 45469#0: *3151 open() "/var/www/html/404.html" failed (2: No such file or directory), client: 193.41.206.24, server: _, request: "GET /debug.php HTTP/1.1", host: "23.43.19.64"
2024/12/25 15:22:07 [error] 45469#0: *3151 open() "/var/www/html/.phpinfo" failed (2: No such file or directory), client: 193.41.206.24, server: _, request: "GET /.phpinfo HTTP/1.1", host: "23.43.19.64"
2024/12/25 15:22:07 [error] 45469#0: *3151 open() "/var/www/html/404.html" failed (2: No such file or directory), client: 193.41.206.24, server: _, request: "GET /.phpinfo HTTP/1.1", host: "23.43.19.64"
2024/12/25 15:22:07 [error] 45469#0: *3151 open() "/var/www/html/404.html" failed (2: No such file or directory), client: 193.41.206.24, server: _, request: "GET /_profiler/phpinfo/info.php HTTP/1.1", host: "23.43.19.64"
2024/12/25 15:22:07 [error] 45469#0: *3151 open() "/var/www/html/404.html" failed (2: No such file or directory), client: 193.41.206.24, server: _, request: "GET /_profiler/phpinfo/phpinfo.php HTTP/1.1", host: "23.43.19.64"
2024/12/25 15:22:07 [error] 45469#0: *3151 open() "/var/www/html/404.html" failed (2: No such file or directory), client: 193.41.206.24, server: _, request: "GET /admin/info.php HTTP/1.1", host: "23.43.19.64"
2024/12/25 15:22:07 [error] 45469#0: *3151 open() "/var/www/html/404.html" failed (2: No such file or directory), client: 193.41.206.24, server: _, request: "GET /adminphp.php HTTP/1.1", host: "23.43.19.64"
2024/12/25 15:22:07 [error] 45469#0: *3151 open() "/var/www/html/404.html" failed (2: No such file or directory), client: 193.41.206.24, server: _, request: "GET /adminphp.php/configuration.php HTTP/1.1", host: "23.43.19.64"
2024/12/25 15:22:07 [error] 45469#0: *3151 open() "/var/www/html/404.html" failed (2: No such file or directory), client: 193.41.206.24, server: _, request: "GET /apache.php HTTP/1.1", host: "23.43.19.64"
2024/12/25 15:22:07 [error] 45469#0: *3151 open() "/var/www/html/404.html" failed (2: No such file or directory), client: 193.41.206.24, server: _, request: "GET /apache/i.php HTTP/1.1", host: "23.43.19.64"
2024/12/25 15:22:07 [error] 45469#0: *3151 open() "/var/www/html/404.html" failed (2: No such file or directory), client: 193.41.206.24, server: _, request: "GET /apache/info.php HTTP/1.1", host: "23.43.19.64"
2024/12/25 15:22:07 [error] 45469#0: *3151 open() "/var/www/html/404.html" failed (2: No such file or directory), client: 193.41.206.24, server: _, request: "GET /apache/phpinfo.php HTTP/1.1", host: "23.43.19.64"
2024/12/25 15:35:07 [error] 45469#0: *3152 open() "/var/www/html/.env" failed (2: No such file or directory), client: 66.63.187.168, server: _, request: "GET /.env HTTP/1.1", host: "23.43.19.64"
Let's create filter to match such rows and to get the hostile IP address. Lets use regex101.com to test the regexp.
# /etc/fail2ban/filter.d/nginx-notfound.conf
# Reload fail2ban configs after editing: sudo fail2ban-client reload
# or ban whole IP range now: sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="47.248.0.0/13" reject'
[Definition]
# <HOST> regexp is: (?:::f{4,6}:)?(?P<host>\S+)
failregex = ^.* (\[error]) .*(2: No such file or directory).*client: <HOST>, .* request:.*(phpinfo|adminphp|admin|profiler|phpinfo|debug|apache|.env).*(HTTP\/1.1)
ignoreregex =
datepattern = {^LN-BEG}
Here is a regexp which matches ALL failed request, so it might match something in your application if you have erroneus routes in it!
Use this only in separate jail with limited ban time like maxretry = 3 bantime = 10m to not ban you cusomers!
failregexp = ^.* (\[error]) .*(2: No such file or directory).*client: (?:::f{4,6}:)?(?P<host>\S+), .*
Be careful not to write anything that matches your applications normal URLs or requests, so you don't ban your customers!
For failed GET/POST /login requests of your application, you should write custom rate limiting rules to allow some failed logins at least.
Then add the jail rule how these hostile requests should be banned.
# /etc/fail2ban/jail.local
[nginx-notfound]
enabled = true
# port = http,https
port = all
logpath = /var/log/nginx/error.log
findtime = 1m
maxretry = 3
bantime = -1
This Jail will allow 3 matching request in 1 minute and ban forever.
Here we have some lines from access.log when someone wants to ruin our christmas day :)
47.251.13.59 - - [25/Dec/2024:17:10:19 +0200] "GET /dns-query?name=example.com&type=A HTTP/1.1" 301 185 "-" "Go-http-client/1.1" "-"
47.251.13.59 - - [25/Dec/2024:17:10:19 +0200] "GET /query?dns=JQABAAABAAAAAAAAB2V4YW1wbGUDY29tAAABAAE HTTP/1.1" 301 185 "-" "Go-http-client/1.1" "-"
47.251.13.59 - - [25/Dec/2024:17:10:19 +0200] "POST /query HTTP/1.1" 301 185 "-" "Go-http-client/1.1" "-"
47.251.13.59 - - [25/Dec/2024:17:10:20 +0200] "GET /query?name=example.com&type=A HTTP/1.1" 301 185 "-" "Go-http-client/1.1" "-"
47.251.13.59 - - [25/Dec/2024:17:10:20 +0200] "GET /query?dns=XiYBAAABAAAAAAAAB2V4YW1wbGUDY29tAAABAAE HTTP/1.1" 301 185 "-" "Go-http-client/1.1" "-"
47.251.13.59 - - [25/Dec/2024:17:10:20 +0200] "POST /query HTTP/1.1" 301 185 "-" "Go-http-client/1.1" "-"
47.251.13.59 - - [25/Dec/2024:17:10:20 +0200] "GET /query?name=example.com&type=A HTTP/1.1" 301 185 "-" "Go-http-client/1.1" "-"
47.251.13.59 - - [25/Dec/2024:17:10:20 +0200] "GET /resolve?dns=2kkBAAABAAAAAAAAB2V4YW1wbGUDY29tAAABAAE HTTP/1.1" 301 185 "-" "Go-http-client/1.1" "-"
47.251.13.59 - - [25/Dec/2024:17:10:20 +0200] "POST /resolve HTTP/1.1" 301 185 "-" "Go-http-client/1.1" "-"
47.251.13.59 - - [25/Dec/2024:17:10:21 +0200] "GET /resolve?name=example.com&type=A HTTP/1.1" 301 185 "-" "Go-http-client/1.1" "-"
47.251.13.59 - - [25/Dec/2024:17:10:21 +0200] "GET /resolve?dns=LeQBAAABAAAAAAAAB2V4YW1wbGUDY29tAAABAAE HTTP/1.1" 301 185 "-" "Go-http-client/1.1" "-"
47.251.13.59 - - [25/Dec/2024:17:10:21 +0200] "POST /resolve HTTP/1.1" 301 185 "-" "Go-http-client/1.1" "-"
Regular expression test page here regex101.com
Fail2ban filter to find the hostile IP addresses of matching rows.
# /etc/fail2ban/filter.d/nginx-scanners.conf
[Definition]
# <HOST> regexp is: (?:::f{4,6}:)?(?P<host>\S+)
failregex = ^<HOST>.* (\"GET|"POST) .*(dns-query|query|resolve|dns|\?name=)
^<HOST>.* ("POST \/ HTTP\/1.1" 301 185)
ignoreregex =
To manually ban some larger IP ranges, try these steps:
- Check where the hacker IP address is coming from iplocation.net
- Check what IP range that is in the country list ip2location.com
- Get the CIDR of the range and ban it in your firewall mikero.com/misc/ipcalc
Use firewall-cmd to ban the IP range
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="95.141.0.0/16" reject'