Reviewing the Apache access log is one of the fastest ways to spot reconnaissance, brute-force probing, and exploit traffic before it turns into an outage or a confirmed compromise. A short audit pass shows which clients are hitting the server, which URIs they are targeting, and whether the traffic pattern looks like routine browsing or automated abuse.
Apache writes access records through mod_log_config. Each CustomLog directive points to a file, and the linked LogFormat decides which request fields are recorded. Current upstream Apache documentation still describes the familiar combined layout with client address, request line, status, byte count, referrer, and user-agent, while current Ubuntu and Debian packages still ship CustomLog ${APACHE_LOG_DIR}/access.log combined by default. Those fields are enough to pivot by source IP, HTTP method, target path, response code, and suspicious payload fragments.
Accurate interpretation still depends on the logging path, proxy layout, and retention window on the host you are auditing. Rotated logs may hold the earlier part of an incident, compressed files need zgrep or zless, and the first field can be a reverse proxy rather than the real client unless mod_remoteip or an equivalent trusted-proxy setup is in place. Pattern matches are leads rather than proof, so validate suspicious hits by reviewing the full request line and the matching Apache error-log window.
$ sudo grep --recursive --line-number --extended-regexp '^[[:space:]]*(LogFormat|CustomLog)[[:space:]]' /etc/apache2
/etc/apache2/apache2.conf:212:LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
/etc/apache2/apache2.conf:213:LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
/etc/apache2/sites-available/000-default.conf:21: CustomLog ${APACHE_LOG_DIR}/access.log combined
/etc/apache2/conf-available/other-vhosts-access-log.conf:2:CustomLog ${APACHE_LOG_DIR}/other_vhosts_access.log vhost_combined
On RHEL-family systems, search /etc/httpd/ instead and expect log files such as /var/log/httpd/access_log plus the httpd service name.
Related: Location for Apache configuration
$ sudo ls -lh /var/log/apache2/access.log* -rw-r----- 1 root adm 62K Apr 9 04:57 /var/log/apache2/access.log -rw-r----- 1 root adm 248K Apr 8 00:00 /var/log/apache2/access.log.1 -rw-r----- 1 root adm 31K Apr 1 00:00 /var/log/apache2/access.log.2.gz
Use zgrep or zless on compressed rotations so older probe traffic is not missed just because it has already been archived.
$ sudo tail -n 3 /var/log/apache2/access.log 203.0.113.45 - - [09/Apr/2026:04:57:41 +0000] "PROPFIND / HTTP/1.1" 405 495 "-" "Mozilla/5.0 (X11; Linux x86_64)" 198.51.100.23 - - [09/Apr/2026:04:57:44 +0000] "GET /search?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E HTTP/1.1" 404 432 "-" "curl/8.7.1" 203.0.113.45 - - [09/Apr/2026:04:57:46 +0000] "GET /wp-login.php HTTP/1.1" 404 432 "-" "Mozilla/5.0 (X11; Linux x86_64)"
The request line is enclosed in quotes, the status code follows it, and the first field is the client address recorded by the current log format. Current Ubuntu and Debian packages use %O in the packaged combined format, while the upstream example uses %b, so the byte field may include response headers on some hosts.
$ sudo awk '{print $1}' /var/log/apache2/access.log | sort | uniq -c | sort -nr | head -10
13 203.0.113.45
2 198.51.100.23
1 66.249.66.1
1 52.167.144.1
If a reverse proxy or load balancer sits in front of Apache, confirm whether this first field is the real client or the proxy address before blocking anything.
$ sudo awk '{print $6}' /var/log/apache2/access.log | tr -d '"' | sort | uniq -c | sort -nr
15 GET
1 PROPFIND
1 OPTIONS
1 PUT
Rare methods such as PROPFIND, PUT, DELETE, TRACE, or CONNECT often come from scanning tools, WebDAV probes, or misconfigured clients rather than normal browser traffic.
$ sudo awk '$9 == 404 {print $7}' /var/log/apache2/access.log | sort | uniq -c | sort -nr | head -15
1 /wp-login.php
1 /xmlrpc.php
1 /phpmyadmin/
1 /.env
1 /%2e%2e%2f%2e%2e%2fetc%2fpasswd
1 /search?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E
Clusters of 404 hits against WordPress, admin panels, framework files, backup files, or traversal strings usually indicate reconnaissance rather than a human browsing session.
$ sudo awk '$9 == 401 || $9 == 403 {print $1, $7, $9}' /var/log/apache2/access.log | head -10
203.0.113.45 /server-status 403
198.51.100.23 /admin 401
Large runs of 401 responses against a login path can indicate password spraying, and repeated 403 hits against admin or status URLs usually mean the client is enumerating restricted resources.
$ sudo grep -Ei '"(GET|POST) /(wp-login\.php|xmlrpc\.php|\.env|phpmyadmin|\.git|admin|setup|config)' /var/log/apache2/access.log | head -8 203.0.113.45 - - [09/Apr/2026:04:57:46 +0000] "GET /wp-login.php HTTP/1.1" 404 432 "-" "Mozilla/5.0 (X11; Linux x86_64)" 203.0.113.45 - - [09/Apr/2026:04:57:47 +0000] "GET /.env HTTP/1.1" 404 432 "-" "Mozilla/5.0 (X11; Linux x86_64)" 203.0.113.45 - - [09/Apr/2026:04:57:48 +0000] "GET /phpmyadmin/ HTTP/1.1" 404 432 "-" "Mozilla/5.0 (X11; Linux x86_64)"
Any 200 or 206 response for high-risk paths such as /.env or /.git needs immediate investigation because the request may have reached sensitive content instead of a harmless 404.
$ sudo grep -Ei '(\.\./|%2e%2e%2f|%2fetc%2fpasswd|%3cscript%3e|<script|union(\+|%20|[[:space:]])+select|or(\+|%20|[[:space:]])+1=1|sleep(\(|%28)|benchmark(\(|%28)|cmd=|exec=|%3b|%7c|%60|\$\(|%24%28)' /var/log/apache2/access.log | head -10 198.51.100.23 - - [09/Apr/2026:04:57:44 +0000] "GET /search?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E HTTP/1.1" 404 432 "-" "curl/8.7.1" 203.0.113.45 - - [09/Apr/2026:04:57:49 +0000] "GET /%2e%2e%2f%2e%2e%2fetc%2fpasswd HTTP/1.1" 404 432 "-" "Mozilla/5.0 (X11; Linux x86_64)" 203.0.113.45 - - [09/Apr/2026:04:57:50 +0000] "GET /product?id=1%20union%20select%201,2,3 HTTP/1.1" 404 432 "-" "Mozilla/5.0 (X11; Linux x86_64)"
These patterns are intentionally broad. Tune them for the application on the host so normal search queries or API parameters are not mistaken for exploitation.
$ sudo grep -Ei '(\.\./|%2e%2e%2f|%2fetc%2fpasswd|%3cscript%3e|<script|union(\+|%20|[[:space:]])+select|or(\+|%20|[[:space:]])+1=1|sleep(\(|%28)|benchmark(\(|%28)|cmd=|exec=|%3b|%7c|%60|\$\(|%24%28)' /var/log/apache2/access.log | awk '{print $1}' | sort | uniq -c | sort -nr | head -10
13 203.0.113.45
2 198.51.100.23
This gives you a short list of clients worth deeper review before you export evidence or add a temporary block.
$ sudo grep '^203.0.113.45 ' /var/log/apache2/access.log | awk '{print $7}' | sort | uniq -c | sort -nr | head -15
2 /
1 /wp-login.php
1 /xmlrpc.php
1 /server-status
1 /.env
1 /product?id=1%20union%20select%201,2,3
Replace 203.0.113.45 with an address from the previous suspect list so the next review stays focused on one source at a time.
$ sudo grep '\[09/Apr/2026:04:57:' /var/log/apache2/access.log | head -6 203.0.113.45 - - [09/Apr/2026:04:57:41 +0000] "PROPFIND / HTTP/1.1" 405 495 "-" "Mozilla/5.0 (X11; Linux x86_64)" 203.0.113.45 - - [09/Apr/2026:04:57:44 +0000] "GET /wp-login.php HTTP/1.1" 404 432 "-" "Mozilla/5.0 (X11; Linux x86_64)" 203.0.113.45 - - [09/Apr/2026:04:57:47 +0000] "GET /.env HTTP/1.1" 404 432 "-" "Mozilla/5.0 (X11; Linux x86_64)"
Matching on [DD/Mon/YYYY:HH:MM: keeps the filter aligned with the Apache timestamp format and helps correlate with firewall, WAF, or application events.
$ sudo tail -n 20 /var/log/apache2/error.log ##### snipped ##### [Thu Apr 09 04:57:44.521313 2026] [authz_core:error] [pid 14927:tid 14963] [client 203.0.113.45:0] AH01630: client denied by server configuration: /var/www/html/server-status ##### snipped #####
Matching timestamps, client addresses, and request paths across the access log and error log give much stronger evidence than a one-line pattern hit by itself.