DEV Community

Log Audit
Log Audit

Posted on

How to Detect Credential Stuffing Attacks in Your Nginx Logs

How to Detect Credential Stuffing Attacks in Your Nginx Logs

Credential stuffing is when attackers take leaked username/password lists from data breaches and try them automatically against your login endpoint. Unlike brute force (guessing random passwords), credential stuffing uses real credentials — which makes it far more dangerous and harder to spot.

The good news: it leaves a very clear pattern in your nginx logs.

What Credential Stuffing Looks Like

A normal login attempt looks like this:

192.168.1.1 - - [20/Mar/2026:09:15:00 +0000] "POST /api/login HTTP/1.1" 200 156
Enter fullscreen mode Exit fullscreen mode

A credential stuffing attack looks like this:

185.220.101.34 - - [20/Mar/2026:09:15:01 +0000] "POST /api/login HTTP/1.1" 401 53
185.220.101.34 - - [20/Mar/2026:09:15:02 +0000] "POST /api/login HTTP/1.1" 401 53
185.220.101.34 - - [20/Mar/2026:09:15:02 +0000] "POST /api/login HTTP/1.1" 401 53
185.220.101.34 - - [20/Mar/2026:09:15:03 +0000] "POST /api/login HTTP/1.1" 401 53
185.220.101.34 - - [20/Mar/2026:09:15:03 +0000] "POST /api/login HTTP/1.1" 200 156
Enter fullscreen mode Exit fullscreen mode

Key signals:

  • Same IP, rapid succession — dozens of 401s in seconds
  • Single endpoint — always hitting /login, /api/auth, /signin
  • Eventually a 200 — when they find a valid credential
  • No other activity — not browsing the site, just hammering the endpoint

Detecting It with grep and awk

# Find IPs with many failed logins (401s) to login endpoint
grep 'POST /api/login' /var/log/nginx/access.log | \
  grep ' 401 ' | \
  awk '{print $1}' | \
  sort | uniq -c | sort -rn | head -20

# Find IPs with 10+ failed logins
grep 'POST /api/login' /var/log/nginx/access.log | \
  grep ' 401 ' | \
  awk '{print $1}' | \
  sort | uniq -c | \
  awk '$1 >= 10 {print $1, $2}' | sort -rn

# Check if any of those IPs then got a 200 (successful login)
SUSPECT_IP="185.220.101.34"
grep "$SUSPECT_IP.*POST /api/login" /var/log/nginx/access.log | \
  awk '{print $9}' | sort | uniq -c
Enter fullscreen mode Exit fullscreen mode

Detecting Distributed Credential Stuffing

Sophisticated attackers rotate IPs to avoid per-IP rate limits. This is harder to catch but still has a pattern:

# High volume of 401s across many IPs in a short window
grep 'POST /api/login' /var/log/nginx/access.log | \
  grep ' 401 ' | \
  awk '{print $4}' | \
  cut -d: -f1-3 | \
  sort | uniq -c | sort -rn | head -10
# Shows 401 counts per hour — a spike = distributed attack

# Count unique IPs hitting login in last hour vs normal
grep 'POST /api/login' /var/log/nginx/access.log | \
  awk -v date="$(date -u +'%d/%b/%Y:%H')" '$4 ~ date {print $1}' | \
  sort -u | wc -l
Enter fullscreen mode Exit fullscreen mode

Block It with fail2ban

Create /etc/fail2ban/filter.d/nginx-login.conf:

[Definition]
failregex = ^<HOST> .* "POST /api/login HTTP.+" 401
ignoreregex =
Enter fullscreen mode Exit fullscreen mode

Add to /etc/fail2ban/jail.local:

[nginx-login]
enabled  = true
port     = http,https
filter   = nginx-login
logpath  = /var/log/nginx/access.log
maxretry = 10
bantime  = 3600
findtime = 60
Enter fullscreen mode Exit fullscreen mode

This bans any IP that fails login 10 times within 60 seconds for 1 hour.

systemctl restart fail2ban
fail2ban-client status nginx-login
Enter fullscreen mode Exit fullscreen mode

nginx Rate Limiting

For defense-in-depth, add rate limiting directly in nginx:

# In http block
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;

# In server block
location /api/login {
    limit_req zone=login burst=10 nodelay;
    limit_req_status 429;
    # ... rest of config
}
Enter fullscreen mode Exit fullscreen mode

This limits each IP to 5 login attempts per minute with a burst of 10.

Warning Signs You've Been Hit

Check these after finding a stuffing campaign:

# Any successful logins from attacking IPs?
ATTACKER_IPS=$(grep 'POST /api/login' /var/log/nginx/access.log | \
  grep ' 401 ' | awk '{print $1}' | sort | uniq -c | \
  awk '$1 >= 20 {print $2}')

for IP in $ATTACKER_IPS; do
  SUCCESS=$(grep "$IP.*POST /api/login" /var/log/nginx/access.log | grep ' 200 ')
  if [ -n "$SUCCESS" ]; then
    echo "COMPROMISED: $IP got a 200"
    echo "$SUCCESS"
  fi
done
Enter fullscreen mode Exit fullscreen mode

If you find successful logins from attacking IPs, those accounts are compromised and need immediate password resets.

Beyond grep: Automated Detection

Manual log analysis works for incident response but you need continuous monitoring to catch attacks as they happen — not hours later.

LogAudit automatically detects credential stuffing patterns in your nginx logs in real time, alerting you the moment an attack campaign starts rather than after accounts are compromised. Free tier available.


Found this useful? The next post covers GDPR risks hiding in your nginx logs — specifically what PII you might be accidentally logging in query strings.

Top comments (0)