Configure NGINX with comprehensive rate limiting, connection throttling, and advanced security headers to protect your web applications from DDoS attacks and malicious traffic. Learn to implement zone-based rate limiting, geographic blocking, and real-time monitoring for production environments.
Prerequisites
- Root or sudo access
- Basic understanding of NGINX configuration
- Familiarity with iptables firewall rules
What this solves
DDoS attacks and malicious traffic can overwhelm your web servers, causing service outages and performance degradation. This tutorial shows you how to configure NGINX with advanced rate limiting, connection controls, and security headers to protect your applications from various attack vectors including volumetric attacks, application-layer attacks, and bot traffic.
Step-by-step configuration
Update system packages
Start by updating your package manager to ensure you get the latest NGINX version with security patches.
sudo apt update && sudo apt upgrade -y
Install NGINX with additional modules
Install NGINX along with the GeoIP module for geographic blocking capabilities.
sudo apt install -y nginx nginx-module-geoip
sudo nginx -V
Configure main NGINX security settings
Create the main NGINX configuration with security-focused settings and rate limiting zones.
user www-data;
worker_processes auto;
worker_rlimit_nofile 65535;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 4096;
use epoll;
multi_accept on;
}
http {
# Basic settings
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 30;
keepalive_requests 1000;
types_hash_max_size 2048;
server_tokens off;
# Rate limiting zones
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=api:10m rate=20r/s;
limit_req_zone $binary_remote_addr zone=global:10m rate=10r/s;
limit_req_zone $server_name zone=perserver:10m rate=100r/s;
# Connection limiting
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver_conn:10m;
# Request size limits
client_max_body_size 10M;
client_body_buffer_size 128k;
client_header_buffer_size 3m;
large_client_header_buffers 4 256k;
# Timeout settings
client_body_timeout 10s;
client_header_timeout 10s;
send_timeout 10s;
# Security headers map
map $sent_http_content_type $expires {
default off;
text/html epoch;
text/css max;
application/javascript max;
~image/ 1M;
}
expires $expires;
# Logging
log_format security '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" urt="$upstream_response_time"';
access_log /var/log/nginx/access.log security;
error_log /var/log/nginx/error.log warn;
# MIME types
include /etc/nginx/mime.types;
default_type application/octet-stream;
# SSL settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Include server configurations
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Create DDoS protection configuration
Create a dedicated configuration file for advanced DDoS protection rules.
# Block common attack patterns
map $http_user_agent $blocked_agent {
default 0;
~*malicious 1;
~*attack 1;
~*hack 1;
"" 1;
}
Block suspicious request methods
map $request_method $blocked_method {
default 0;
~*^(TRACE|DELETE|PATCH)$ 1;
}
Rate limiting for different endpoints
map $request_uri $request_uri_path {
default "global";
~^/login "login";
~^/api/ "api";
~^/admin "login";
~^/wp-admin "login";
~^/wp-login "login";
}
Geographic blocking (requires GeoIP database)
Uncomment and configure based on your needs
map $geoip_country_code $blocked_country {
default 0;
~^(CN|RU|KP)$ 1;
}
Custom error pages for rate limiting
error_page 429 /429.html;
error_page 444 /444.html;
Configure virtual host with security rules
Create a virtual host configuration that implements the DDoS protection measures.
server {
listen 80;
listen 443 ssl http2;
server_name example.com www.example.com;
# SSL configuration (adjust paths as needed)
ssl_certificate /etc/ssl/certs/example.com.crt;
ssl_certificate_key /etc/ssl/private/example.com.key;
ssl_dhparam /etc/ssl/certs/dhparam.pem;
# Root directory
root /var/www/html;
index index.html index.php;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Connection and request limits
limit_conn perip 10;
limit_conn perserver_conn 1000;
# Block malicious agents and methods
if ($blocked_agent) {
return 444;
}
if ($blocked_method) {
return 405;
}
# Block empty user agents
if ($http_user_agent = "") {
return 444;
}
# Block requests without Host header
if ($host !~* ^(example\.com|www\.example\.com)$) {
return 444;
}
# Rate limiting for login endpoints
location ~* ^/(login|admin|wp-admin|wp-login) {
limit_req zone=login burst=3 nodelay;
limit_req_status 429;
# Additional security for admin areas
# Uncomment to restrict by IP
# allow 203.0.113.10;
# deny all;
try_files $uri $uri/ =404;
}
# Rate limiting for API endpoints
location ^~ /api/ {
limit_req zone=api burst=50 nodelay;
limit_req_status 429;
# API-specific headers
add_header X-RateLimit-Limit "20/s" always;
add_header X-RateLimit-Remaining $limit_req_status always;
try_files $uri $uri/ =404;
}
# Main location with global rate limiting
location / {
limit_req zone=global burst=20 nodelay;
limit_req_status 429;
# Block common exploit attempts
location ~* \.(php|asp|aspx|jsp)$ {
return 444;
}
# Block access to sensitive files
location ~* \.(sql|bak|backup|log|old|tmp|temp)$ {
return 444;
}
# Block dot files
location ~ /\. {
return 444;
}
try_files $uri $uri/ =404;
}
# Custom error pages
location = /429.html {
root /var/www/errors;
internal;
}
location = /444.html {
root /var/www/errors;
internal;
}
# Security-focused error handling
error_page 400 401 402 403 404 /40x.html;
error_page 500 502 503 504 /50x.html;
location = /40x.html {
root /var/www/errors;
internal;
}
location = /50x.html {
root /var/www/errors;
internal;
}
}
Create custom error pages
Create custom error pages to avoid revealing server information during attacks.
sudo mkdir -p /var/www/errors
sudo chown www-data:www-data /var/www/errors
Rate Limited
Too Many Requests
You have been rate limited. Please try again later.
Access Denied
Access Denied
The requested resource is not available.
Configure log rotation for security monitoring
Set up log rotation to manage the increased logging from security events.
/var/log/nginx/*.log {
daily
missingok
rotate 30
compress
delaycompress
notifempty
create 644 www-data adm
sharedscripts
prerotate
if [ -d /etc/logrotate.d/httpd-prerotate ]; then \
run-parts /etc/logrotate.d/httpd-prerotate; \
fi \
endscript
postrotate
invoke-rc.d nginx rotate >/dev/null 2>&1
endscript
}
Create monitoring script for attack detection
Create a script to monitor for potential attacks and suspicious activity.
#!/bin/bash
NGINX Attack Monitoring Script
LOG_FILE="/var/log/nginx/access.log"
ALERT_THRESHOLD=100
TIME_WINDOW=60 # seconds
BAN_TIME=3600 # 1 hour
TEMP_FILE="/tmp/nginx-attacks.tmp"
Function to ban IP using iptables
ban_ip() {
local ip=$1
echo "$(date): Banning IP $ip for $BAN_TIME seconds" >> /var/log/nginx-monitor.log
iptables -I INPUT -s $ip -j DROP
# Schedule unban
echo "sleep $BAN_TIME && iptables -D INPUT -s $ip -j DROP" | at now
}
Monitor for rate limit violations (429 status)
if [ -f "$LOG_FILE" ]; then
# Get IPs with excessive 429 responses in the last minute
tail -n 10000 $LOG_FILE | \
awk -v threshold="$ALERT_THRESHOLD" -v window="$TIME_WINDOW" '
BEGIN {
now = systime()
}
{
# Parse timestamp and convert to epoch
gsub(/\[|\]/, "", $4)
gsub(/:/, " ", $4)
gsub(/\//, " ", $4)
cmd = "date -d \"" $4 "\" +%s"
cmd | getline timestamp
close(cmd)
# Check if within time window and status is 429
if (now - timestamp <= window && $9 == "429") {
count[$1]++
}
}
END {
for (ip in count) {
if (count[ip] >= threshold) {
print ip, count[ip]
}
}
}' > $TEMP_FILE
# Ban IPs that exceed threshold
while read ip count; do
if [ ! -z "$ip" ]; then
# Check if IP is not already banned
if ! iptables -L INPUT | grep -q $ip; then
ban_ip $ip
fi
fi
done < $TEMP_FILE
rm -f $TEMP_FILE
fi
sudo chmod +x /usr/local/bin/nginx-monitor.sh
sudo chown root:root /usr/local/bin/nginx-monitor.sh
Set up automated monitoring
Configure a cron job to run the monitoring script regularly.
sudo crontab -e
# Monitor for NGINX attacks every minute
* /usr/local/bin/nginx-monitor.sh
Clean up old monitor logs weekly
0 2 0 find /var/log -name "nginx-monitor.log" -mtime +7 -delete
Enable and start services
Enable NGINX and test the configuration before starting.
sudo nginx -t
sudo ln -sf /etc/nginx/sites-available/secure-site /etc/nginx/sites-enabled/
sudo systemctl enable nginx
sudo systemctl restart nginx
sudo systemctl status nginx
Configure advanced security monitoring
Install and configure Fail2Ban integration
Integrate Fail2Ban with NGINX logs for automated IP banning based on attack patterns.
sudo apt install -y fail2ban
[nginx-rate-limit]
enabled = true
filter = nginx-rate-limit
logpath = /var/log/nginx/access.log
maxretry = 10
findtime = 60
bantime = 3600
action = iptables[name=nginx-rate-limit, port=http, protocol=tcp]
[nginx-bad-request]
enabled = true
filter = nginx-bad-request
logpath = /var/log/nginx/access.log
maxretry = 5
findtime = 60
bantime = 3600
action = iptables[name=nginx-bad-request, port=http, protocol=tcp]
[Definition]
failregex = ^ . "." 429 .*$
ignoreregex =
[Definition]
failregex = ^ . "." (400|401|403|444) .*$
ignoreregex =
Start Fail2Ban service
Enable and start the Fail2Ban service to begin monitoring.
sudo systemctl enable fail2ban
sudo systemctl restart fail2ban
sudo systemctl status fail2ban
Verify your setup
# Check NGINX configuration
sudo nginx -t
Verify rate limiting zones are loaded
sudo nginx -T | grep -A 5 limit_req_zone
Check active connections and limits
curl -I http://example.com
curl -w "%{http_code}\n" -s -o /dev/null http://example.com
Test rate limiting (should return 429 after limits)
for i in {1..25}; do curl -s -o /dev/null -w "%{http_code} " http://example.com; done
echo
Check Fail2Ban status
sudo fail2ban-client status
sudo fail2ban-client status nginx-rate-limit
Monitor real-time logs
sudo tail -f /var/log/nginx/access.log | grep -E '(429|444|403)'
Check current iptables rules
sudo iptables -L INPUT -n
Performance tuning and optimization
For high-traffic sites, you may need to adjust the rate limiting parameters and connection limits based on your specific requirements. Monitor your server resources and adjust the worker processes and connection limits accordingly.
Consider implementing NGINX reverse proxy with SSL termination for better performance in clustered environments. For comprehensive monitoring, integrate with Prometheus and Grafana monitoring to track attack patterns and server performance metrics.
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Configuration test fails | Syntax error in NGINX config | sudo nginx -t to identify specific errors |
| Rate limiting not working | Zone not defined or incorrect syntax | Check limit_req_zone definitions and limit_req usage |
| All requests blocked | Rate limits too restrictive | Increase burst values or rate limits in configuration |
| 429 errors for legitimate users | Rate limits too aggressive | Adjust rates and implement whitelist for trusted IPs |
| Fail2Ban not banning IPs | Log format doesn't match filter regex | Test filters with fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-rate-limit.conf |
| High memory usage | Rate limiting zones too large | Reduce zone sizes or implement zone cleanup |
| SSL/TLS errors | Certificate configuration issues | Check certificate paths and permissions with nginx -t |
| Geographic blocking not working | GeoIP database not installed | Install GeoIP database and configure module properly |
Next steps
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Global variables
BACKUP_DIR="/root/nginx-security-backup-$(date +%Y%m%d-%H%M%S)"
NGINX_USER=""
SITES_DIR=""
# Error handler
cleanup() {
if [ -d "$BACKUP_DIR" ]; then
echo -e "${YELLOW}[ROLLBACK] Restoring original configuration from $BACKUP_DIR${NC}"
cp -r "$BACKUP_DIR"/* /etc/nginx/
systemctl restart nginx || true
fi
}
trap cleanup ERR
usage() {
cat << EOF
Usage: $0 [OPTIONS]
Configure NGINX with advanced rate limiting and DDoS protection
OPTIONS:
-d, --domain DOMAIN Primary domain name (optional)
-h, --help Show this help message
Examples:
$0 -d example.com
$0 --domain mysite.org
EOF
exit 1
}
log() {
echo -e "${GREEN}$1${NC}"
}
warn() {
echo -e "${YELLOW}$1${NC}"
}
error() {
echo -e "${RED}$1${NC}" >&2
}
# Parse arguments
DOMAIN=""
while [[ $# -gt 0 ]]; do
case $1 in
-d|--domain)
DOMAIN="$2"
shift 2
;;
-h|--help)
usage
;;
*)
error "Unknown option: $1"
usage
;;
esac
done
echo -e "${GREEN}[1/8] Checking prerequisites${NC}"
if [[ $EUID -ne 0 ]]; then
error "This script must be run as root"
exit 1
fi
# Detect distribution and set package manager
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update && apt upgrade -y"
NGINX_USER="www-data"
SITES_DIR="/etc/nginx/sites-available"
GEOIP_PKG="nginx-module-geoip"
;;
almalinux|rocky|centos|rhel|ol)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf update -y"
NGINX_USER="nginx"
SITES_DIR="/etc/nginx/conf.d"
GEOIP_PKG="nginx-mod-http-geoip"
;;
fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf update -y"
NGINX_USER="nginx"
SITES_DIR="/etc/nginx/conf.d"
GEOIP_PKG="nginx-mod-http-geoip"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum update -y"
NGINX_USER="nginx"
SITES_DIR="/etc/nginx/conf.d"
GEOIP_PKG="nginx-mod-http-geoip"
;;
*)
error "Unsupported distribution: $ID"
exit 1
;;
esac
else
error "Cannot detect distribution"
exit 1
fi
log "[2/8] Creating backup of existing configuration"
mkdir -p "$BACKUP_DIR"
if [ -d /etc/nginx ]; then
cp -r /etc/nginx/* "$BACKUP_DIR/"
log "Backup created at $BACKUP_DIR"
fi
log "[3/8] Updating system packages"
$PKG_UPDATE
log "[4/8] Installing NGINX with GeoIP module"
$PKG_INSTALL nginx $GEOIP_PKG
# Enable NGINX service
systemctl enable nginx
log "[5/8] Configuring main NGINX security settings"
cat > /etc/nginx/nginx.conf << 'EOF'
user nginx;
worker_processes auto;
worker_rlimit_nofile 65535;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
load_module modules/ngx_http_geoip_module.so;
events {
worker_connections 4096;
use epoll;
multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 30;
keepalive_requests 1000;
types_hash_max_size 2048;
server_tokens off;
# Rate limiting zones
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=api:10m rate=20r/s;
limit_req_zone $binary_remote_addr zone=global:10m rate=10r/s;
limit_req_zone $server_name zone=perserver:10m rate=100r/s;
# Connection limiting
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver_conn:10m;
# Request size limits
client_max_body_size 10M;
client_body_buffer_size 128k;
client_header_buffer_size 3m;
large_client_header_buffers 4 256k;
# Timeout settings
client_body_timeout 10s;
client_header_timeout 10s;
send_timeout 10s;
# Security headers
map $sent_http_content_type $expires {
default off;
text/html epoch;
text/css max;
application/javascript max;
~image/ 1M;
}
expires $expires;
# Logging format
log_format security '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time';
access_log /var/log/nginx/access.log security;
error_log /var/log/nginx/error.log warn;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# SSL settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
EOF
# Fix user directive for detected distribution
sed -i "s/^user nginx;/user $NGINX_USER;/" /etc/nginx/nginx.conf
log "[6/8] Creating DDoS protection configuration"
cat > /etc/nginx/conf.d/ddos-protection.conf << 'EOF'
# Block malicious user agents
map $http_user_agent $blocked_agent {
default 0;
~*malicious 1;
~*attack 1;
~*hack 1;
"" 1;
}
# Block suspicious methods
map $request_method $blocked_method {
default 0;
~*^(TRACE|DELETE|PATCH)$ 1;
}
# Rate limiting by endpoint
map $request_uri $rate_limit_zone {
default "global";
~^/login "login";
~^/api/ "api";
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
EOF
log "[7/8] Creating example server configuration"
if [ -n "$DOMAIN" ]; then
SERVER_NAME="$DOMAIN"
else
SERVER_NAME="_"
fi
cat > "$SITES_DIR/default-secure.conf" << EOF
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name $SERVER_NAME;
# DDoS protection
if (\$blocked_agent) { return 403; }
if (\$blocked_method) { return 405; }
# Rate limiting
limit_req zone=global burst=20 nodelay;
limit_conn perip 10;
# Security headers
include /etc/nginx/conf.d/ddos-protection.conf;
location / {
root /var/www/html;
index index.html index.htm;
}
location /login {
limit_req zone=login burst=5 nodelay;
proxy_pass http://127.0.0.1:8080;
}
location /api/ {
limit_req zone=api burst=50 nodelay;
proxy_pass http://127.0.0.1:8080;
}
# Block access to sensitive files
location ~* \.(htaccess|htpasswd|ini|log|sh)$ {
deny all;
}
}
EOF
# Create sites-enabled directory and symlink for Debian-based systems
if [[ "$ID" =~ ^(ubuntu|debian)$ ]]; then
mkdir -p /etc/nginx/sites-enabled
ln -sf "$SITES_DIR/default-secure.conf" /etc/nginx/sites-enabled/
# Remove default site
rm -f /etc/nginx/sites-enabled/default
fi
# Create basic index page
mkdir -p /var/www/html
cat > /var/www/html/index.html << 'EOF'
<!DOCTYPE html>
<html>
<head><title>NGINX Security Configured</title></head>
<body>
<h1>NGINX with DDoS Protection</h1>
<p>Your server is now configured with advanced security features.</p>
</body>
</html>
EOF
# Set proper permissions
chown -R "$NGINX_USER:$NGINX_USER" /var/www/html
chmod -R 755 /var/www/html
chmod 644 /var/www/html/index.html
# Set proper permissions for NGINX config files
chown -R root:root /etc/nginx
chmod 755 /etc/nginx
chmod 644 /etc/nginx/nginx.conf
chmod 644 /etc/nginx/conf.d/*.conf
log "[8/8] Testing configuration and starting NGINX"
nginx -t
# Configure firewall if available
if command -v ufw >/dev/null 2>&1; then
ufw --force enable
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
elif command -v firewall-cmd >/dev/null 2>&1; then
systemctl enable --now firewalld
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --permanent --add-service=ssh
firewall-cmd --reload
fi
systemctl restart nginx
systemctl status nginx
log "✅ NGINX security configuration completed successfully!"
log "📁 Configuration backup saved to: $BACKUP_DIR"
log "🔒 Rate limiting zones configured: login (5r/m), api (20r/s), global (10r/s)"
log "🛡️ DDoS protection active with suspicious user agent and method blocking"
log "📊 Security logging enabled in /var/log/nginx/"
if [ -n "$DOMAIN" ]; then
log "🌐 Configured for domain: $DOMAIN"
fi
warn "⚠️ Remember to:"
warn " - Configure SSL certificates for production use"
warn " - Adjust rate limits based on your application needs"
warn " - Monitor logs in /var/log/nginx/ for blocked requests"
warn " - Test your application functionality with the new limits"
Review the script before running. Execute with: bash install.sh