Zwei Überwachungskameras an einer grauen Ziegelsteinwand.

Minimising log noise: Blocking blind botnets and malicious probes with Fail2ban

Anyone who runs a publicly accessible web server is familiar with this phenomenon: a glance at the log files reveals accesses every second to files such as wp-login.php, .env or obscure paths that do not even exist on one’s own server. These access attempts are rarely targeted, human-led hacking attacks. They are the constant, automated background noise of the internet: so-called blind bots and malicious probes.

In this post, we’ll look at why these scans are a problem and how we can efficiently block them using Fail2ban before they affect the performance of our Apache web server.

The problem with automated scans

These botnets operate, quite literally, ‘blindly’. They scan the IPv4 and IPv6 address ranges en masse, taking a shot in the dark. They do not know which applications are actually running on the target system, but instead automatically search for known vulnerabilities in content management systems (such as WordPress), exposed configuration files (.env) or misconfigurations.

Even if a single HTTP 404 error (Not Found) seems harmless, these scans quickly add up to a significant problem:

  1. Resource consumption: Every request must be received, processed and logged by the Apache web server. During aggressive bot attacks, this ties up CPU and RAM. Furthermore, depending on the configuration, a database entry is created in the sys_log table for each of these requests, resulting in additional server load and resource consumption.
  2. Log spamming: Analysing access and error logs becomes extremely difficult when legitimate errors are buried under thousands of lines of automated junk.
  3. Increased security risk: If a bot happens to find an unprotected vulnerability (e.g. a forgotten backup file or an old script), the system is compromised fully automatically in a fraction of a second.

Example: Log files on a server hosting a TYPO3 instance: The basic security principle is that there is absolutely no legitimate reason why a client should access directories such as /wp-content or /wp-admin, or files such as .env. Massive numbers of requests for non-existent PHP files are also a clear indication that the server is being scanned by bots. Such requests are, by definition, to be classified as malicious.

Example entries from an Apache log file

 

20.197.180.144 - - [22/Jun/2026:17:18:36 +0200] "GET /wp-admin/css/colors/midnight/about.php HTTP/1.1" 401 381 "-" "-"
20.197.180.144 - - [22/Jun/2026:17:18:37 +0200] "GET /bgymj.php HTTP/1.1" 401 381 "-" "-"
20.197.180.144 - - [22/Jun/2026:17:18:37 +0200] "GET /xx.php HTTP/1.1" 401 381 "-" "-"
20.197.180.144 - - [22/Jun/2026:17:18:37 +0200] "GET /file58.php HTTP/1.1" 401 381 "-" "-"
20.197.180.144 - - [22/Jun/2026:17:18:37 +0200] "GET /wp-links.php HTTP/1.1" 401 381 "-" "-"
20.197.180.144 - - [22/Jun/2026:17:18:38 +0200] "GET /solo1.php HTTP/1.1" 401 381 "-" "-"
20.197.180.144 - - [22/Jun/2026:17:18:38 +0200] "GET /flower.php HTTP/1.1" 401 381 "-" "-"
20.197.180.144 - - [22/Jun/2026:17:18:38 +0200] "GET /3.php HTTP/1.1" 401 381 "-" "-"
20.197.180.144 - - [22/Jun/2026:17:18:38 +0200] "GET /orm.php HTTP/1.1" 401 381 "-" "-"
20.197.180.144 - - [22/Jun/2026:17:18:38 +0200] "GET /ot.php HTTP/1.1" 401 381 "-" "-"
20.197.180.144 - - [22/Jun/2026:17:18:39 +0200] "GET /sixxis.php HTTP/1.1" 401 381 "-" "-"
20.197.180.144 - - [22/Jun/2026:17:18:39 +0200] "GET /2P.update.php HTTP/1.1" 401 381 "-" "-"
20.197.180.144 - - [22/Jun/2026:17:18:39 +0200] "GET /f3027655e14136.php HTTP/1.1" 401 381 "-" "-"
20.197.180.144 - - [22/Jun/2026:17:18:39 +0200] "GET /25d653587fdfd1.php HTTP/1.1" 401 381 "-" "-"
20.197.180.144 - - [22/Jun/2026:17:18:39 +0200] "GET /f35.php HTTP/1.1" 401 381 "-" "-"
20.104.244.165 - - [23/Jun/2026:02:42:05 +0200] "GET /wp-admin/ HTTP/1.1" 301 245 "-" "-"
20.104.244.165 - - [23/Jun/2026:02:42:05 +0200] "GET /wp-admin/ HTTP/1.1" 401 381 "-" "-"
20.104.244.165 - - [23/Jun/2026:02:42:11 +0200] "GET /wp-admin/wp.php HTTP/1.1" 301 251 "-" "-"
20.104.244.165 - - [23/Jun/2026:02:42:12 +0200] "GET /wp-admin/wp.php HTTP/1.1" 401 381 "-" "-"
20.104.244.165 - - [23/Jun/2026:02:42:23 +0200] "GET /wp-admin/js/ HTTP/1.1" 301 248 "-" "-"
20.104.244.165 - - [23/Jun/2026:02:42:23 +0200] "GET /wp-admin/js/ HTTP/1.1" 401 381 "-" "-"
20.104.244.165 - - [23/Jun/2026:02:42:27 +0200] "GET /wp-admin/js/index.php HTTP/1.1" 301 257 "-" "-"
20.104.244.165 - - [23/Jun/2026:02:42:27 +0200] "GET /wp-admin/js/index.php HTTP/1.1" 401 381 "-" "-"

 

Prevention options: Proactive blocking at network level

To reduce the load on the web server and isolate attackers at an early stage, simply serving an error page is not enough. The most effective solution is dynamic blocking at the network level (firewall) using a log analyser such as Fail2ban.

Fail2ban monitors the log files in real time, detects suspicious behaviour patterns using regular expressions (Regex) and temporarily blocks the offending IP address via UFW (Uncomplicated Firewall), iptables or nftables.

Practical implementation: filter and jail configuration

1. The filter: Example in /etc/fail2ban/filter.d/apache-phpscan.conf

This filter searches the access log for two patterns:

  • Requests for PHP files that fail (HTTP status code not equal to 200, e.g. 403, 404, 301, 302).
  • Explicit access attempts to highly sensitive paths (WordPress structures, environment variables, Exchange Autodiscover), regardless of the status code returned. As we do not serve these paths, every request is classified as a malicious scan. The regular expressions may need to be adapted to the log format in each case, particularly if a different web server or proxy, such as nginx, is used with modified log files.
[Definition]
failregex = ^<host> - - \[.*\] "(?:GET|POST|HEAD) \S*\.php\S* HTTP/\d+(?:\.\d+)*" (?:30[12]|40[0134])
            ^<host> - - \[.*\] "(?:GET|POST|HEAD) /+wp-content\S* HTTP/\d+(?:\.\d+)*" \d+
            ^<host> - - \[.*\] "(?:GET|POST|HEAD) /+wp-admin\S* HTTP/\d+(?:\.\d+)*" \d+
            ^<host> - - \[.*\] "(?:GET|POST|HEAD) /+wp-includes\S* HTTP/\d+(?:\.\d+)*" \d+
            ^<host> - - \[.*\] "(?:GET|POST|HEAD) /+\.env\S* HTTP/\d+(?:\.\d+)*" \d+
            ^<host> - - \[.*\] "(?:GET|POST|HEAD) (?i)/+autodiscover/autodiscover\.xml\S* HTTP/\d+(?:\.\d+)*" \d+
ignoreregex =</host></host></host></host></host></host>

 

2. The jail: /etc/fail2ban/jail.d/apache-phpscan.local

The associated jail defines the log paths and the severity of the ban. In our example: if an IP address generates more than 5 hits (maxretry) within 10 seconds (findtime), it is completely blocked for 5 hours (bantime = 18000) via an entry in the firewall.

 

[apache-phpscan]
enabled  = true
port     = http,https
filter   = apache-phpscan
logpath  = /var/log/httpd/access_log
          /var/log/httpd/another_access_log
backend = auto action = iptables-multiport[port="http,https"] findtime = 10 maxretry = 5 bantime = 18000

 

Conclusion & Best Practice

With minimal configuration effort, this Fail2ban setup ensures that automated scanners hit a digital wall after just a few requests. This conserves the Apache web server’s resources and keeps the log files clean.

Important practical tip before going live: Always test your configuration in advance using the built-in Fail2ban tool against your actual log files to ensure that the regular expressions match correctly:

 

fail2ban-regex /var/log/httpd/access_log /etc/fail2ban/filter.d/apache-phpscan.conf

 

Other helpful tips:

 

# Liste aller durch den Fail2Ban Filter erzeugen Einträge
iptables -L f2b-apache-phpscan -n -v

# Status und Statistiken des Fail2Ban Filters ausgeben
fail2ban-client status apache-phpscan