HTTP Flood Attack Defense in Practice: Nginx Multi-Layer Rate Limiting Configuration
HTTP flood attacks exhaust server resources with large volumes of seemingly legitimate HTTP requests. Covers Nginx limit_req, limit_conn, connection limits, and UA filtering for multi-layer defense, plus an access.log attack-signature detection script.
Scope
HTTP flood is a common DDoS form. Attackers send large volumes of seemingly legitimate GET/POST requests from botnets or cloud hosts, exhausting CPU, memory, or bandwidth so legitimate users cannot access the service. Applies to Nginx reverse-proxy environments: CMS, APIs, e-commerce.
Prerequisites
- Nginx 1.18+ (ngx_http_limit_req_module and ngx_http_limit_conn_module compiled in)
- Python 3.6+ (log analysis script)
- Server root
How it works
HTTP flood differs from Slowloris. Slowloris holds connections open to exhaust the pool; HTTP flood sends complete HTTP requests at high speed. Distinguishing signals: RPS and request-signature distribution. Defense compares traffic to a normal-user baseline, then rate-limits anomalous high-frequency clients.
Procedure
Step 1: Nginx base rate-limit config
Define zones in the `nginx.conf` http block:
# /etc/nginx/nginx.conf -- inside http block
# Request rate-limit zone: by client IP
# rate=30r/s -- average max 30 requests per second
# burst=50 -- burst buffer of 50 requests
# nodelay -- process burst immediately (reject overflow)
limit_req_zone $binary_remote_addr zone=per_ip:10m rate=30r/s;
# Concurrent connection limit: max 20 connections per IP
limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
# Server-level rate limit (global)
limit_req_zone $server_name zone=global:10m rate=500r/s;
Enable in the target server/location:
# /etc/nginx/conf.d/default.conf -- inside server block
server {
listen 443 ssl;
server_name your-site.com;
limit_req zone=per_ip burst=50 nodelay;
limit_conn conn_per_ip 20;
limit_req zone=global burst=1000 nodelay;
limit_req_status 429;
limit_conn_status 429;
error_page 429 /429.html;
location = /429.html {
root /var/www/html;
internal;
}
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Stricter limits on login/API endpoints
location /wp-login.php {
limit_req zone=per_ip burst=5 nodelay;
limit_conn conn_per_ip 5;
proxy_pass http://backend;
}
location /api/ {
limit_req zone=per_ip burst=10 nodelay;
limit_conn conn_per_ip 10;
proxy_pass http://backend_api;
}
}
Step 2: UA and request-signature filtering
Attackers often reuse a fixed or obviously fake User-Agent. Nginx UA blacklist:
# UA blacklist map
map $http_user_agent $bad_ua {
default 0;
"~*curl" 1;
"~*wget" 1;
"~*python-requests" 1;
"~*Go-http-client" 1;
"~*Java/" 1;
"~*libwww-perl" 1;
"~*HTTPie" 1;
"" 1;
}
server {
if ($bad_ua) { return 403; }
}
Use UA filters carefully; they false-positive crawlers and API clients. Prefer combined rate + UA checks.
Step 3: Path-based rate limiting
limit_req_zone $binary_remote_addr zone=static:10m rate=100r/s;
limit_req_zone $binary_remote_addr zone=dynamic:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=search:10m rate=5r/s;
server {
# Static assets -- looser limit
location ~* \.(jpg|jpeg|png|gif|css|js|ico|woff2?)$ {
limit_req zone=static burst=200 nodelay;
expires 30d;
add_header Cache-Control "public";
}
# Dynamic pages -- moderate limit
location / {
limit_req zone=dynamic burst=20 nodelay;
}
# Search/query -- strict limit
location ~* /(search|query|s\.php) {
limit_req zone=search burst=5 nodelay;
}
}
Step 4: Log analysis script
Python script against Nginx access.log: high-frequency IPs and attack signatures.
#!/usr/bin/env python3
"""
Nginx HTTP Flood attack signature analysis script
Usage: python3 flood_detect.py /var/log/nginx/access.log
"""
import sys, re, collections
LOG_PATTERN = re.compile(
r'(?P[\d.]+) .* \[(?P[^\]]+)\] '
r'"(?P\w+) (?P\S+) .*" '
r'(?P\d+) (?P\d+)'
)
def parse_log(filepath, window_secs=60, threshold=100):
ip_counter = collections.Counter()
ip_details = {}
with open(filepath, 'r') as f:
for line in f:
m = LOG_PATTERN.search(line)
if not m:
continue
ip = m.group('ip')
ip_counter[ip] += 1
if ip not in ip_details:
ip_details[ip] = {
'paths': collections.Counter(),
'methods': collections.Counter(),
'statuses': collections.Counter(),
}
ip_details[ip]['paths'][m.group('path')] += 1
ip_details[ip]['methods'][m.group('method')] += 1
ip_details[ip]['statuses'][m.group('status')] += 1
print("{: threshold * 5:
flags.append("high-freq attack")
if details['statuses'].get('404', 0) > count * 0.3:
flags.append("scanner")
if details['statuses'].get('403', 0) > count * 0.5:
flags.append("blocked")
if details['methods']['POST'] > count * 0.5:
flags.append("mass POST")
flag_str = " [" + ", ".join(flags) + "]" if flags else ""
print("{: [window_secs] [threshold]")
sys.exit(1)
window = int(sys.argv[2]) if len(sys.argv) > 2 else 60
threshold = int(sys.argv[3]) if len(sys.argv) > 3 else 100
parse_log(sys.argv[1], window, threshold)
Step 5: Auto-ban script
#!/bin/bash
# auto_ban.sh -- auto-ban high-frequency IPs
LOG="/var/log/nginx/access.log"
BAN_LIST="/etc/nginx/conf.d/ban_ip.conf"
THRESHOLD=500
WINDOW=60
python3 flood_detect.py "$LOG" "$WINDOW" "$THRESHOLD" | grep "high-freq attack" | \
awk '{print $1}' | while read ip; do
if ! grep -q "$ip" "$BAN_LIST" 2>/dev/null; then
echo "deny $ip;" >> "$BAN_LIST"
echo "[BAN] $ip"
fi
done
nginx -s reload
Add to cron, every 5 minutes:
*/5 * * * * /usr/local/bin/auto_ban.sh >> /var/log/auto_ban.log 2>&1
Verification
# 1. Check Nginx config syntax
nginx -t
# 2. Simulate high-frequency requests with ab
ab -n 1000 -c 50 https://your-site.com/
# 3. Test per-IP rate limit
for i in $(seq 1 100); do
curl -s -o /dev/null -w "%{http_code}\n" https://your-site.com/ &
done
# expect some 429s
# 4. Check 429 records in access.log
grep "429" /var/log/nginx/access.log | head -10
FAQ
Q1: How do limit_req burst and nodelay interact?
`burst=N` queues requests that instantaneously exceed rate; above N they are rejected. Without `nodelay`, queued requests drain at `rate` (adds latency). With `nodelay`, queued requests are processed immediately; overflow beyond burst is rejected. Use `nodelay` on APIs. Omit `nodelay` on normal page traffic for smoother limiting.
Q2: After rate limiting, how do you separate a legitimate burst from an attack?
Three signals: path distribution (attackers concentrate on a few paths), User-Agent diversity (attack UAs are often single or obviously fake), source IP distribution (CDN origin IPs are stable; attack IPs are scattered). Return 429 (not 403) to suspects, then a frontend JS challenge (reCAPTCHA) to confirm.
Summary
HTTP flood defense stacks Nginx limiters: `limit_req` (request rate), `limit_conn` (concurrency), UA filtering, and log-based adaptive bans. No universal config; tune against real traffic. Combine with CDN WAF and cloud DDoS (Cloudflare, Alibaba Cloud Anti-DDoS).