How to audit Apache access logs for security threats

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.

Steps to audit Apache access logs for security threats:

  1. Identify the active Apache access-log files before searching blindly.
    $ 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.

  2. List the current and rotated copies of the access log so you know how much incident history is available.
    $ 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.

  3. Preview recent lines to confirm the real field order before building filters.
    $ 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.

  4. Count the highest-volume source IPs to spot scanners and abusive clients quickly.
    $ 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.

  5. Count HTTP methods so unexpected verbs stand out immediately.
    $ 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.

  6. Review the most-requested missing pages to see which applications or admin paths are being probed.
    $ 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.

  7. Review authentication and authorization failures for credential stuffing or permission probing.
    $ 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.

  8. Search for common exploit-target paths and configuration leaks directly in the request line.
    $ 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.

  9. Search for encoded payloads that suggest traversal, XSS, SQL injection, or command-injection testing.
    $ 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.

  10. Build a quick suspect list by counting which IPs hit those high-risk patterns most often.
    $ 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.

  11. Pivot into one suspect IP to see which URIs it is hitting most often.
    $ 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.

  12. Narrow the log to the incident window when the suspicious activity occurred.
    $ 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.

  13. Correlate the suspicious requests with the Apache error log before treating the pattern as a confirmed incident.
    $ 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.