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.
$ 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.
$ 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.
$ 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.
$ 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.
$ 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.
$ 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.
$ 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.
$ 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.
$ 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.
$ 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.
$ 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.
$ 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.
$ 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.
Related: How to restrict access by IP in Nginx
Related: How to block user agents in Nginx