Authentication endpoints have two attack surfaces simultaneously: credential stuffing (an account takeover attack) and compute exhaustion (a DDoS attack). Both require the same defense: strict per-IP and per-username rate limiting at the edge, before your bcrypt or Argon2 password hashing runs. Each rule below addresses a specific failure mode observed in production environments.
Credential stuffing volumes have increased substantially as breach databases have become more accessible. A typical modern stuffing campaign runs 50,000 to 500,000 attempts per hour using rotating residential proxies, keeping each individual IP below naive rate limit thresholds. Effective defense requires layered rules, not a single high threshold.
Cloudflare: Login Endpoint Rules
# Rule 1: Strict per-IP rate limit on login (POST only)
Name: Login POST rate limit
Expression: (http.request.uri.path eq "/api/v1/auth/login")
AND (http.request.method eq "POST")
Action: Block
Threshold: 5
Period: 60 seconds
Mitigation timeout: 600 seconds
Counting dimension: ip
# Rule 2: Challenge on bot score below threshold
Name: Login bot challenge
Expression: (http.request.uri.path eq "/api/v1/auth/login")
AND (cf.bot_management.score lt 30)
Action: Managed Challenge
Priority: higher than Rule 1
# Rule 3: Block known bad ASNs on auth endpoints
Name: Login ASN blocklist
Expression: (http.request.uri.path contains "/auth/")
AND (ip.geoip.asnum in {16509 14618 15169})
# AWS/GCP/Google ASNs on auth = suspicious
Action: Block
Why 5 attempts per 60 seconds?
A legitimate user attempting to log in will make at most 2-3 attempts in 60 seconds before stopping to reset their password. 5 attempts is generous for human users and prohibitive for automated credential stuffing at scale.
AWS WAF: Auth Endpoint Protection
# Rate-based rule targeting login POST
{
"Name": "LoginRateLimit",
"Priority": 1,
"Statement": {
"RateBasedStatement": {
"Limit": 100,
"AggregateKeyType": "IP",
"ScopeDownStatement": {
"AndStatement": {
"Statements": [
{
"ByteMatchStatement": {
"SearchString": "/auth/login",
"FieldToMatch": {"UriPath": {}},
"TextTransformations": [{"Priority":0,"Type":"LOWERCASE"}],
"PositionalConstraint": "CONTAINS"
}
},
{
"ByteMatchStatement": {
"SearchString": "POST",
"FieldToMatch": {"Method": {}},
"TextTransformations": [{"Priority":0,"Type":"NONE"}],
"PositionalConstraint": "EXACTLY"
}
}
]
}
}
}
},
"Action": {"Block": {}},
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "LoginRateLimit"
}
}
Nginx: Auth Rate Limiting with Penalty Box
http {
# Auth endpoint zone — 5 requests/minute per IP
limit_req_zone $binary_remote_addr zone=auth_zone:10m rate=5r/m;
# Penalty zone — IPs that exceeded auth limit stay blocked 10 minutes
# (Nginx does not natively support penalty boxes; use fail2ban integration)
server {
location ~ ^/api/v[0-9]+/auth/ {
limit_req zone=auth_zone burst=2 nodelay;
limit_req_status 429;
# JSON error body for API clients
error_page 429 = @auth_rate_limited;
}
location @auth_rate_limited {
default_type application/json;
return 429 '{"error":"rate_limited","retry_after":60}';
}
}
}
What Rules Alone Do Not Solve
IP-based rate limits are insufficient against large residential proxy networks where each IP stays below the threshold. Layer in: username-level rate limiting in application code (track failed attempts per username, lock after 10 failures in 10 minutes), CAPTCHA or Turnstile challenge after first failed attempt from an IP, and monitoring on the failed-to-successful login ratio as an operational metric. A ratio above 50:1 indicates an ongoing stuffing campaign even if no individual IP is triggering rate limits.
Test Whether Your Auth Rules Fire
DDactic probes your authentication endpoints with calibrated request sequences and reports whether rate limit rules fire at the thresholds you expect.
Run a Free Scan