Skip to content

Instantly share code, notes, and snippets.

@jarkko-hautakorpi
Last active December 25, 2024 19:33
Show Gist options
  • Save jarkko-hautakorpi/f402d7138eddf1c1358d28653966380f to your computer and use it in GitHub Desktop.
Save jarkko-hautakorpi/f402d7138eddf1c1358d28653966380f to your computer and use it in GitHub Desktop.
Creating new fail2ban filter regular expressions

fail2ban filters quick tutorial (failregex)

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.

The regexp

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.

Nginx error.log

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.

Nginx access.log

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 =

Manual IP address range blocking

To manually ban some larger IP ranges, try these steps:

  1. Check where the hacker IP address is coming from iplocation.net
  2. Check what IP range that is in the country list ip2location.com
  3. 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'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment