How to audit Nginx access logs for security threats

Reviewing the Nginx access log is one of the fastest ways to spot reconnaissance, brute-force probing, exploit traffic, and other abusive request patterns before they turn into an outage or a confirmed compromise.

Current Nginx logging behavior is controlled by access_log and log_format. Upstream Nginx documentation still defines access_log in the http, server, and location contexts, still uses the predefined combined format by default when no custom format is named, and still supports non-file destinations such as syslog. Current Ubuntu packages still write the default access log to /var/log/nginx/access.log, and standard Debian family layouts commonly use the same path, which is usually the first place to audit a live host.

Accurate interpretation still depends on the log destination, proxy chain, and retention window on the host being audited. Rotated logs may hold the earlier part of an incident, compressed files need zgrep or zless, and the first address field can be a reverse proxy rather than the real client unless trusted-proxy handling rewrites it. Treat pattern matches as leads rather than proof, and confirm them against the matching Nginx error log, WAF audit log, or application log before blocking real traffic.

Steps to audit Nginx access logs for security threats:

  1. Identify the active Nginx access-log destination before searching blindly.
    $ sudo nginx -T 2>/dev/null | grep -nE '^\s*access_log\b'
    41:    access_log /var/log/nginx/access.log;

    Current Nginx documentation still allows access_log to be set in the http, server, or location context, so a virtual host can override the global path. If the destination is syslog:…, stderr, or a container stream, audit that sink instead of assuming a regular file.

  2. List the current and rotated copies of the access log so the available audit window is clear.
    $ sudo ls -lh /var/log/nginx/access.log*
    -rw-r----- 1 www-data adm  72K Apr  9 13:28 /var/log/nginx/access.log
    -rw-r----- 1 www-data adm 254K Apr  8 00:00 /var/log/nginx/access.log.1
    -rw-r----- 1 www-data adm  31K Apr  1 00:00 /var/log/nginx/access.log.2.gz

    Use zgrep or zless on compressed rotations so earlier probe traffic is not missed just because it has already been archived.

  3. Preview recent lines to confirm the field order before building filters.
    $ sudo tail -n 4 /var/log/nginx/access.log
    127.0.0.1 - - [09/Apr/2026:13:28:06 +0000] "GET /.env HTTP/1.1" 404 162 "-" "Mozilla/5.0 (X11; Linux x86_64)"
    127.0.0.1 - - [09/Apr/2026:13:28:06 +0000] "GET /search?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E HTTP/1.1" 404 162 "-" "curl/8.7.1"
    127.0.0.1 - - [09/Apr/2026:13:28:06 +0000] "GET /product?id=1%20union%20select%201,2,3 HTTP/1.1" 404 162 "-" "Mozilla/5.0 (X11; Linux x86_64)"
    127.0.0.1 - - [09/Apr/2026:13:28:06 +0000] "GET /admin/ HTTP/1.1" 403 162 "-" "Mozilla/5.0 (X11; Linux x86_64)"

    In the predefined combined format, the request line is quoted, the status code follows it, and the first field is the client address recorded by the current log format. If a custom log_format changes that order, adapt the field numbers in the next steps before trusting the output.

  4. Count the highest-volume source IP addresses to spot scanners and abusive clients quickly.
    $ sudo awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head -10
         14 203.0.113.45
          3 198.51.100.23
          1 66.249.66.1

    If Nginx sits behind a load balancer or another reverse proxy, confirm whether the first field is the real client or the proxy address before blocking anything.

  5. Review the most-requested missing pages to see which applications or admin paths are being probed.
    $ sudo awk '$9 == 404 {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head -15
          3 /wp-login.php
          2 /xmlrpc.php
          1 /phpmyadmin/
          1 /.env
          1 /.git/config

    Clusters of 404 hits against WordPress, admin panels, framework files, backup files, or leaked repository paths usually indicate reconnaissance rather than routine browsing.

  6. Count HTTP methods so unexpected verbs stand out immediately.
    $ sudo awk '{print $6}' /var/log/nginx/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 WebDAV probes, scanners, or misconfigured clients rather than normal browser traffic.

  7. Review authentication and authorization failures for credential stuffing or permission probing.
    $ sudo awk '$9 == 401 || $9 == 403 {print $1, $7, $9}' /var/log/nginx/access.log | head -10
    127.0.0.1 /admin/ 403

    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/nginx/access.log | head -8
    203.0.113.45 - - [09/Apr/2026:04:57:46 +0000] "GET /wp-login.php HTTP/1.1" 404 162 "-" "Mozilla/5.0 (X11; Linux x86_64)"
    203.0.113.45 - - [09/Apr/2026:04:57:47 +0000] "GET /.env HTTP/1.1" 404 162 "-" "Mozilla/5.0 (X11; Linux x86_64)"
    203.0.113.45 - - [09/Apr/2026:04:57:48 +0000] "GET /phpmyadmin/ HTTP/1.1" 404 162 "-" "Mozilla/5.0 (X11; Linux x86_64)"

    Any 200, 204, 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 XSS, SQL injection, traversal, or command-injection testing.
    $ sudo grep -Ei '(%3cscript%3e|<script|union(\+|%20|[[:space:]])+select|or(\+|%20|[[:space:]])+1=1|\.\./|%2e%2e%2f|%2fetc%2fpasswd|sleep(\(|%28)|benchmark(\(|%28)|cmd=|exec=|%3b|%7c|%60|\$\(|%24%28)' /var/log/nginx/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 162 "-" "curl/8.7.1"
    203.0.113.45 - - [09/Apr/2026:04:57:49 +0000] "GET /product?id=1%20union%20select%201,2,3 HTTP/1.1" 404 162 "-" "Mozilla/5.0 (X11; Linux x86_64)"
    203.0.113.45 - - [09/Apr/2026:04:57:50 +0000] "GET /%2e%2e%2f%2e%2e%2fetc%2fpasswd HTTP/1.1" 404 162 "-" "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 suspect list by counting which IPs hit those high-risk patterns most often.
    $ sudo grep -Ei '(%3cscript%3e|<script|union(\+|%20|[[:space:]])+select|or(\+|%20|[[:space:]])+1=1|\.\./|%2e%2e%2f|%2fetc%2fpasswd|sleep(\(|%28)|benchmark(\(|%28)|cmd=|exec=|%3b|%7c|%60|\$\(|%24%28)' /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -nr | head -10
         13 203.0.113.45
          2 198.51.100.23

    This produces a short review list before exporting evidence or adding a temporary block.

  11. Pivot into one suspect IP to see which URIs it hits most often.
    $ sudo grep '^203.0.113.45 ' /var/log/nginx/access.log | awk '{print $7}' | sort | uniq -c | sort -nr | head -15
          2 /
          1 /wp-login.php
          1 /.env
          1 /product?id=1%20union%20select%201,2,3
          1 /admin/

    Replace 203.0.113.45 with an address from the 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:13:28:' /var/log/nginx/access.log | head -6
    127.0.0.1 - - [09/Apr/2026:13:28:06 +0000] "PROPFIND / HTTP/1.1" 405 166 "-" "Mozilla/5.0 (X11; Linux x86_64)"
    127.0.0.1 - - [09/Apr/2026:13:28:06 +0000] "GET /wp-login.php HTTP/1.1" 404 162 "-" "Mozilla/5.0 (X11; Linux x86_64)"
    127.0.0.1 - - [09/Apr/2026:13:28:06 +0000] "GET /admin/ HTTP/1.1" 403 162 "-" "Mozilla/5.0 (X11; Linux x86_64)"

    Matching on [DD/Mon/YYYY:HH:MM: stays aligned with the Nginx timestamp format and makes cross-correlation with firewall, WAF, or application logs easier.

  13. Correlate suspicious 403 denials or other anomalies with the Nginx error log before treating the pattern as a confirmed incident.
    $ sudo tail -n 20 /var/log/nginx/error.log
    2026/04/09 13:28:06 [error] 3268#3268: *6 access forbidden by rule, client: 127.0.0.1, server: _, request: "GET /admin/ HTTP/1.1", host: "127.0.0.1:8080"

    Matching timestamps, client addresses, and request paths across the access and error logs give much stronger evidence than a one-line pattern hit by itself.

    Use the journal or the central log collector instead when the deployment sends errors to stderr or syslog instead of /var/log/nginx/error.log.