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
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
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
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
Block It with fail2ban
Create /etc/fail2ban/filter.d/nginx-login.conf:
[Definition]
failregex = ^<HOST> .* "POST /api/login HTTP.+" 401
ignoreregex =
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
This bans any IP that fails login 10 times within 60 seconds for 1 hour.
systemctl restart fail2ban
fail2ban-client status nginx-login
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
}
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
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)