2026年11月12日
2026年11月12日
Detect Attacks in Apache Logs: grep, awk and fail2ban Auto-Blocking
This article is a localized edition of the Japanese original. Read the Japanese original
Most attacks against a web server leave traces in Apache's access log and error log. This article walks through where the logs are and how to read them, commands for finding signs of attacks, and fail2ban settings that block the patterns you find automatically. The fail2ban settings were checked against the configuration files in the official fail2ban repository as of November 2026.
Log locations and format
| OS | Access log | Error log |
|---|---|---|
| Debian / Ubuntu | /var/log/apache2/access.log |
/var/log/apache2/error.log |
| RHEL / AlmaLinux / Rocky Linux | /var/log/httpd/access_log |
/var/log/httpd/error_log |
If a virtual host writes to its own file through CustomLog or ErrorLog, look at that file instead. You can find where they are set with grep -R "CustomLog\|ErrorLog" /etc/apache2/ (/etc/httpd/ on RHEL-based systems). On Ubuntu, reading the logs requires root or membership in the adm group.
The access log usually uses the combined format, and each line looks like this:
203.0.113.5 - - [10/Nov/2026:10:15:32 +0900] "POST /wp-login.php HTTP/1.1" 200 4521 "-" "Mozilla/5.0 ..."
Split on spaces, field 1 is the client IP address, field 4 the date and time, field 6 the method (with a leading "), field 7 the path, and field 9 the status code. The awk examples below assume this layout. If you changed LogFormat, or use the vhost_combined format that puts the host name first, the field numbers shift, so adjust them.
Rotated logs remain under names such as access.log.1 and access.log.2.gz. Read compressed files with zcat or zgrep.
Find the IP addresses with the most requests
Start by looking at where most of the traffic comes from.
# Top 20 IP addresses by number of requests
sudo awk '{print $1}' /var/log/apache2/access.log | sort | uniq -c | sort -rn | head -20
# IP addresses receiving many 404s (a sign of scans probing for files that don't exist)
sudo awk '$9 == 404 {print $1}' /var/log/apache2/access.log | sort | uniq -c | sort -rn | head -20
# Look at a specific time window only (e.g. 10:00-10:59 on November 10)
sudo grep '10/Nov/2026:10:' /var/log/apache2/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head
A high request count alone does not mean an attack. Search engine crawlers and monitoring services also show up near the top, so combine the counts with what was requested, as in the next sections.
Common attack patterns
Brute force against the WordPress login
Repeated POST requests to wp-login.php and xmlrpc.php in a short time may be a password brute-force attack.
sudo grep -E '"POST /(wp-login|xmlrpc)\.php' /var/log/apache2/access.log \
| awk '{print $1}' | sort | uniq -c | sort -rn | head -20
Scans for config files and admin tools
These are requests for files that should never be public and for well-known admin tool paths. If any of them returned status 200, check whether that file is actually readable from outside.
sudo grep -iE '"(GET|POST|HEAD) /[^ ]*(\.env|\.git/|wp-config\.php\.|phpmyadmin|\.sql|\.bak)' \
/var/log/apache2/access.log | awk '{print $1, $7, $9}' | sort | uniq -c | sort -rn | head -30
SQL injection, directory traversal and XSS attempts
These are requests whose path or query string contains SQL syntax, ../, <script> and so on. Also search for the URL-encoded forms (%20, %2e%2e, %3Cscript).
sudo grep -iE 'union(\+|%20| )+select|information_schema|\.\./|%2e%2e|<script|%3Cscript' \
/var/log/apache2/access.log | awk '{print $1, $7, $9}' | head -50
Requests to suspicious upload paths
If .php files under wp-content/uploads/ are being requested, an uploaded backdoor may be in use. If the status is 200, check right away whether that file exists.
sudo grep -E '/wp-content/uploads/[^ ]*\.php' /var/log/apache2/access.log | awk '{print $1, $7, $9}'
Check the error log
The error log records requests denied by access restrictions, PHP errors, mod_security detections and more. Look for messages that suddenly increased compared with normal.
# Follow the latest errors as they arrive
sudo tail -f /var/log/apache2/error.log
# Requests denied by access restrictions
sudo grep "client denied by server configuration" /var/log/apache2/error.log | tail -20
Block automatically with fail2ban
fail2ban watches logs and uses the firewall to ban, for a set time, IP addresses that repeatedly match a condition. On Debian and Ubuntu, install it with the following commands (on RHEL-based systems, install it from the EPEL repository).
sudo apt update
sudo apt install fail2ban
Use the bundled filters
fail2ban ships with filters for Apache. They are defined in /etc/fail2ban/jail.conf, but all of them are disabled. jail.conf is overwritten on updates, so don't edit it directly; put only the jails you want to enable in /etc/fail2ban/jail.local.
| Jail name | Log it reads | What it detects |
|---|---|---|
apache-auth |
Error log | Failed authentication such as Basic authentication |
apache-badbots |
Access log | User-Agents of known malicious crawlers |
apache-noscript |
Error log | Requests to run scripts that don't exist |
apache-overflows |
Error log | Abnormally long requests |
apache-botsearch |
Error log | Requests probing for admin tools and similar |
# /etc/fail2ban/jail.local
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
# Never ban trusted sources (such as your own IP address)
ignoreip = 127.0.0.1/8 ::1
[apache-auth]
enabled = true
backend = auto
[apache-badbots]
enabled = true
backend = auto
fail2ban already has default log locations for both Debian-based and RHEL-based systems, so if your logs are in the standard place you don't need to write logpath. backend = auto explicitly tells fail2ban to read the log files in case the distribution's defaults set it to read the systemd journal.
A filter to stop brute force against the WordPress login
Create a filter that counts POST requests to wp-login.php and xmlrpc.php.
# /etc/fail2ban/filter.d/wordpress-login.conf
[Definition]
failregex = ^<HOST> -[^"]*"POST /(?:wp-login|xmlrpc)\.php
ignoreregex =
This filter doesn't distinguish successful from failed logins; it only counts POST requests. Anyone who exceeds maxretry within findtime is banned, even someone logging in many times with the correct password. Adjust the numbers to how your site is used.
# /etc/fail2ban/jail.local (append)
[wordpress-login]
enabled = true
backend = auto
port = http,https
filter = wordpress-login
logpath = /var/log/apache2/access.log
maxretry = 10
findtime = 10m
bantime = 1h
Before enabling it, use fail2ban-regex to check that the filter matches your own log.
sudo fail2ban-regex /var/log/apache2/access.log /etc/fail2ban/filter.d/wordpress-login.conf
If the "Failregex" count in the output is 0, the log format and the filter don't match.
Apply, check and unban
# Apply the settings
sudo systemctl restart fail2ban
# List the running jails
sudo fail2ban-client status
# IP addresses currently banned by a jail
sudo fail2ban-client status wordpress-login
# Unban an IP address that was banned by mistake
sudo fail2ban-client set wordpress-login unbanip 203.0.113.5
The overall fail2ban setup is also covered in "How to block unauthorized access automatically with fail2ban".
Inspect request contents with mod_security
Log analysis and fail2ban react after an attack has been logged. To inspect the request when it is received, use the Apache module mod_security (ModSecurity) together with its rule set, the OWASP Core Rule Set (CRS). On Debian and Ubuntu it comes in the libapache2-mod-security2 package, and the module is enabled when you install it.
Right after installing, keep the setting at SecRuleEngine DetectionOnly (detect and log only). Check the logs to confirm that legitimate traffic isn't being flagged by mistake, and only then switch to On (block). The WordPress admin screens are prone to false positives, so don't skip this check. Installation and rule writing are covered in "How to set up a WAF with Apache mod_security".
Operational notes
- Dumping a large log with
catscrolls the terminal past what you can read. Narrow it down withgrepandawk, or open it withless. - Behind a load balancer or CDN that uses
X-Forwarded-For, the first field of the log is the proxy's IP address. In that case, configuremod_remoteipto log the real client before you analyze or ban. Banning the proxy's IP address blocks every visitor. - Logs grow every day, so decide on a retention period with logrotate. See "How to rotate Apache logs with logrotate" for the configuration.
- If there are signs that an attack succeeded (a PHP file in
uploadsreturning 200, unknown administrators appearing, and so on), blocking is not enough: you need to investigate the intrusion.
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)