Set up NGINX rate limiting modules, implement connection limits, and configure geographic blocking to protect your web applications from DDoS attacks and abuse.
Prerequisites
- Root or sudo access
- NGINX installed
- Basic understanding of web server configuration
What this solves
Rate limiting and DDoS protection prevent malicious traffic from overwhelming your web servers and degrading service for legitimate users. NGINX's built-in modules let you control request rates, limit concurrent connections, and implement geographic blocking without requiring external services.
Step-by-step configuration
Update system packages and install NGINX
Start by ensuring you have the latest NGINX version with all required modules.
sudo apt update && sudo apt upgrade -y
sudo apt install -y nginx nginx-module-geoip2
sudo dnf update -y
sudo dnf install -y nginx nginx-mod-http-geoip2
Verify NGINX modules are loaded
Check that the rate limiting and GeoIP modules are available for configuration.
nginx -V 2>&1 | grep -o with-http_limit_req_module
nginx -V 2>&1 | grep -o with-http_limit_conn_module
nginx -V 2>&1 | grep -o with-http_geoip2_module
Configure basic rate limiting zones
Define rate limiting zones in the main NGINX configuration. These zones track client IPs and enforce request limits.
http {
# 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=10r/s;
limit_req_zone $binary_remote_addr zone=general:10m rate=1r/s;
# Connection limiting zones
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;
# Define rate limiting log format
log_format rate_limit '$remote_addr - $remote_user [$time_local] '
'"$request" $status $bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" urt="$upstream_response_time"';
# Other existing configuration...
}
Download and configure GeoIP database
Set up geographic blocking by downloading a GeoIP database and configuring NGINX to use it.
sudo mkdir -p /etc/nginx/geoip
cd /etc/nginx/geoip
sudo wget https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb
sudo chown www-data:www-data GeoLite2-Country.mmdb
sudo chmod 644 GeoLite2-Country.mmdb
Add GeoIP configuration to NGINX
Configure NGINX to load the GeoIP database and create country-based variables for blocking.
http {
# Load GeoIP module and database
geoip2 /etc/nginx/geoip/GeoLite2-Country.mmdb {
auto_reload 5m;
$geoip2_metadata_country_build metadata build_epoch;
$geoip2_data_country_code country iso_code;
$geoip2_data_country_name country names en;
}
# Map blocked countries
map $geoip2_data_country_code $blocked_country {
default 0;
CN 1; # China
RU 1; # Russia
KP 1; # North Korea
# Add more country codes as needed
}
# Rate limiting zones (from previous step)
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=general:10m rate=1r/s;
# Connection limiting zones
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;
# Rate limiting log format
log_format rate_limit '$remote_addr - $remote_user [$time_local] '
'"$request" $status $bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" urt="$upstream_response_time" '
'country=$geoip2_data_country_code';
}
Create advanced security configuration
Set up a dedicated configuration file for security rules that can be included in virtual hosts.
# DDoS Protection Configuration
# Block requests with suspicious user agents
map $http_user_agent $blocked_user_agent {
default 0;
~*(nmap|nikto|wikto|sf|sqlmap|bsqlbf|w3af|acunetix|havij|appscan) 1;
~*(hydra|libwww-perl|BBBike|sqlninja) 1;
"" 1; # Block empty user agents
}
# Block common attack patterns in URI
map $request_uri $blocked_uri {
default 0;
~*\.\./\.\./ 1; # Directory traversal
~*union.*select 1; # SQL injection
~*concat.*\( 1; # SQL injection
~*base64_decode 1; # Code injection
~*GLOBALS.*\[ 1; # Global variable access
~*_REQUEST.*\[ 1; # Request variable access
~*proc/self/environ 1; # Environment access
~*auto_prepend_file= 1; # File inclusion
~*auto_append_file= 1; # File inclusion
}
# Rate limiting by request method
map $request_method $rate_limit_key {
GET $binary_remote_addr;
POST $binary_remote_addr;
default '';
}
# Limit request size to prevent large payload attacks
client_max_body_size 10m;
client_body_buffer_size 128k;
client_header_buffer_size 3m;
large_client_header_buffers 4 256k;
# Timeout settings to prevent slowloris attacks
client_body_timeout 10s;
client_header_timeout 10s;
keepalive_timeout 5s 5s;
send_timeout 10s;
Configure a protected virtual host
Create a virtual host configuration that implements all security measures and rate limiting.
server {
listen 80;
server_name example.com;
# Include security rules
include /etc/nginx/conf.d/security-rules.conf;
# Enable rate limiting logs
access_log /var/log/nginx/rate_limit.log rate_limit;
error_log /var/log/nginx/error.log;
# Block based on country
if ($blocked_country) {
return 403 "Access denied from your location";
}
# Block suspicious user agents
if ($blocked_user_agent) {
return 403 "Blocked user agent";
}
# Block suspicious URI patterns
if ($blocked_uri) {
return 403 "Blocked request";
}
# Apply connection limits
limit_conn perip 10; # Max 10 connections per IP
limit_conn perserver 100; # Max 100 connections to server
# Apply general rate limiting
limit_req zone=general burst=5 nodelay;
# Root directory
root /var/www/html;
index index.html index.htm;
# API endpoints with stricter rate limiting
location /api/ {
limit_req zone=api burst=20 nodelay;
limit_req_status 429;
# Add CORS headers for API
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
try_files $uri $uri/ =404;
}
# Login endpoints with very strict rate limiting
location /login {
limit_req zone=login burst=3 nodelay;
limit_req_status 429;
# Additional security headers
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
try_files $uri $uri/ =404;
}
# Static files with relaxed rate limiting
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
limit_req zone=general burst=50 nodelay;
}
# Block access to sensitive files
location ~* \.(htaccess|htpasswd|ini|log|sh|sql|tar|gz)$ {
deny all;
}
# Custom error pages for rate limiting
error_page 429 /429.html;
location = /429.html {
root /var/www/html/error-pages;
internal;
}
error_page 403 /403.html;
location = /403.html {
root /var/www/html/error-pages;
internal;
}
}
Create custom error pages
Set up user-friendly error pages for rate limiting and blocking responses.
sudo mkdir -p /var/www/html/error-pages
sudo chown www-data:www-data /var/www/html/error-pages
<h1>429 - Too Many Requests</h1>
<p>You have exceeded the rate limit. Please wait a moment and try again.</p>
<p>If you believe this is an error, please contact support.</p>
<h1>403 - Forbidden</h1>
<p>Access to this resource is denied.</p>
<p>If you believe this is an error, please contact support.</p>
Set up log rotation for rate limit logs
Configure logrotate to manage the rate limiting log files and prevent disk space issues.
/var/log/nginx/rate_limit.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 640 www-data adm
postrotate
if [ -f /var/run/nginx.pid ]; then
kill -USR1 $(cat /var/run/nginx.pid)
fi
endscript
}
Enable the site and restart NGINX
Activate your protected site configuration and restart NGINX to apply all changes.
sudo nginx -t
sudo ln -s /etc/nginx/sites-available/protected-site /etc/nginx/sites-enabled/
sudo systemctl reload nginx
sudo systemctl status nginx
Configure monitoring alerts
Set up basic monitoring to track rate limiting effectiveness and blocked requests.
server {
listen 8080;
server_name localhost;
location /nginx_status {
stub_status on;
access_log off;
allow 127.0.0.1;
deny all;
}
location /rate_limit_status {
access_log off;
allow 127.0.0.1;
deny all;
default_type text/plain;
return 200 "Rate limiting is active\nCheck /var/log/nginx/rate_limit.log for details\n";
}
}
sudo systemctl reload nginx
Test your rate limiting setup
Verify that rate limiting and security rules are working correctly with these test commands.
# Test general rate limiting (should get 429 after burst limit)
for i in {1..10}; do curl -I http://example.com/; sleep 0.1; done
# Test API rate limiting
for i in {1..25}; do curl -I http://example.com/api/test; sleep 0.1; done
# Test login rate limiting
for i in {1..5}; do curl -I http://example.com/login; sleep 1; done
# Check if suspicious user agent is blocked
curl -H "User-Agent: nmap" http://example.com/
# Verify NGINX status
curl http://localhost:8080/nginx_status
curl http://localhost:8080/rate_limit_status
Monitor rate limiting effectiveness
Use these commands to monitor your rate limiting and identify attack patterns.
# Monitor rate limit log in real-time
sudo tail -f /var/log/nginx/rate_limit.log
# Count rate limited requests in the last hour
sudo grep "$(date -d '1 hour ago' '+%d/%b/%Y:%H')" /var/log/nginx/rate_limit.log | grep -c "429"
# Find top blocked IPs
sudo awk '$9 == "429" {print $1}' /var/log/nginx/rate_limit.log | sort | uniq -c | sort -nr | head -10
# Monitor connection counts
sudo ss -tuln | grep :80
# Check blocked countries
sudo grep "403" /var/log/nginx/access.log | tail -20
Verify your setup
sudo nginx -t
sudo systemctl status nginx
curl -I http://example.com/
sudo tail -n 20 /var/log/nginx/rate_limit.log
sudo ls -la /etc/nginx/geoip/
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Rate limiting not working | Zone not defined in http block | Move limit_req_zone to /etc/nginx/nginx.conf http section |
| GeoIP database not loading | Wrong file permissions | sudo chown www-data:www-data /etc/nginx/geoip/* |
| 429 errors for all requests | Rate limit too restrictive | Increase burst value or rate in zone definition |
| Legitimate traffic blocked | Country blocking too broad | Remove country codes from blocked list or whitelist specific IPs |
| Log files growing too large | No log rotation configured | Ensure /etc/logrotate.d/nginx-rate-limit is properly configured |
| NGINX won't start | Configuration syntax error | Run sudo nginx -t to check configuration |
Next steps
- Monitor NGINX performance with Prometheus and Grafana for comprehensive observability
- Configure centralized logging with rsyslog and logrotate to aggregate security logs
- Set up NGINX with ModSecurity web application firewall for advanced threat protection
- Configure NGINX SSL termination and load balancing for production deployments
- Implement Fail2ban with NGINX security monitoring for automated IP blocking
Running this in production?
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'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Global variables
NGINX_CONFIG=""
NGINX_USER=""
BLOCKED_COUNTRIES="${BLOCKED_COUNTRIES:-CN,RU,KP}"
# Usage function
usage() {
echo "Usage: $0 [OPTIONS]"
echo "Configure NGINX rate limiting and DDoS protection"
echo ""
echo "Options:"
echo " -c COUNTRIES Comma-separated list of country codes to block (default: CN,RU,KP)"
echo " -h Show this help message"
echo ""
echo "Example:"
echo " $0 -c CN,RU,KP,IR"
}
# Parse command line arguments
while getopts "c:h" opt; do
case ${opt} in
c )
BLOCKED_COUNTRIES="$OPTARG"
;;
h )
usage
exit 0
;;
\? )
usage
exit 1
;;
esac
done
# Cleanup function for rollback
cleanup() {
if [[ $? -ne 0 ]]; then
echo -e "${RED}[ERROR] Script failed. Cleaning up...${NC}"
systemctl stop nginx 2>/dev/null || true
if [[ -f "${NGINX_CONFIG}.backup" ]]; then
mv "${NGINX_CONFIG}.backup" "${NGINX_CONFIG}"
echo -e "${YELLOW}[CLEANUP] Restored original nginx.conf${NC}"
fi
systemctl start nginx 2>/dev/null || true
fi
}
trap cleanup ERR
# Check if running as root or with sudo
check_privileges() {
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}[ERROR] This script must be run as root or with sudo${NC}"
exit 1
fi
}
# Auto-detect distribution
detect_distro() {
if [[ ! -f /etc/os-release ]]; then
echo -e "${RED}[ERROR] Cannot detect distribution - /etc/os-release not found${NC}"
exit 1
fi
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update && apt upgrade -y"
PKG_INSTALL="apt install -y"
NGINX_CONFIG="/etc/nginx/nginx.conf"
NGINX_USER="www-data"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
NGINX_CONFIG="/etc/nginx/nginx.conf"
NGINX_USER="nginx"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
NGINX_CONFIG="/etc/nginx/nginx.conf"
NGINX_USER="nginx"
;;
*)
echo -e "${RED}[ERROR] Unsupported distribution: $ID${NC}"
exit 1
;;
esac
echo -e "${GREEN}[INFO] Detected distribution: $PRETTY_NAME${NC}"
}
# Install NGINX and required modules
install_nginx() {
echo -e "${BLUE}[1/6] Installing NGINX and required modules...${NC}"
$PKG_UPDATE
if [[ "$PKG_MGR" == "apt" ]]; then
$PKG_INSTALL nginx nginx-module-geoip2
else
# For RHEL-based systems
if ! rpm -q epel-release >/dev/null 2>&1; then
$PKG_INSTALL epel-release
fi
$PKG_INSTALL nginx nginx-mod-http-geoip2
fi
systemctl enable nginx
systemctl start nginx
}
# Verify NGINX modules
verify_modules() {
echo -e "${BLUE}[2/6] Verifying NGINX modules...${NC}"
if ! nginx -V 2>&1 | grep -q "http_limit_req_module"; then
echo -e "${RED}[ERROR] Rate limiting module not found${NC}"
exit 1
fi
if ! nginx -V 2>&1 | grep -q "http_limit_conn_module"; then
echo -e "${RED}[ERROR] Connection limiting module not found${NC}"
exit 1
fi
echo -e "${GREEN}[SUCCESS] Required modules verified${NC}"
}
# Download GeoIP database
setup_geoip() {
echo -e "${BLUE}[3/6] Setting up GeoIP database...${NC}"
mkdir -p /etc/nginx/geoip
cd /etc/nginx/geoip
if [[ ! -f "GeoLite2-Country.mmdb" ]]; then
wget -q https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb
fi
chown "${NGINX_USER}:${NGINX_USER}" GeoLite2-Country.mmdb
chmod 644 GeoLite2-Country.mmdb
}
# Configure NGINX with security settings
configure_nginx() {
echo -e "${BLUE}[4/6] Configuring NGINX security settings...${NC}"
# Backup original configuration
cp "${NGINX_CONFIG}" "${NGINX_CONFIG}.backup"
# Create security configuration
cat > /etc/nginx/conf.d/security.conf << 'EOF'
# 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=10r/s;
limit_req_zone $binary_remote_addr zone=general:10m rate=1r/s;
# Connection limiting zones
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;
# Load GeoIP module and database
geoip2 /etc/nginx/geoip/GeoLite2-Country.mmdb {
auto_reload 5m;
$geoip2_metadata_country_build metadata build_epoch;
$geoip2_data_country_code country iso_code;
$geoip2_data_country_name country names en;
}
# Map blocked user agents
map $http_user_agent $blocked_agent {
default 0;
~*bot 1;
~*crawler 1;
~*spider 1;
~*scanner 1;
"" 1;
}
# Define rate limiting log format
log_format rate_limit '$remote_addr - $remote_user [$time_local] '
'"$request" $status $bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time '
'country=$geoip2_data_country_code';
EOF
# Create blocked countries map
cat > /etc/nginx/conf.d/geo-blocking.conf << EOF
# Map blocked countries
map \$geoip2_data_country_code \$blocked_country {
default 0;
EOF
IFS=',' read -ra COUNTRIES <<< "$BLOCKED_COUNTRIES"
for country in "${COUNTRIES[@]}"; do
echo " $country 1;" >> /etc/nginx/conf.d/geo-blocking.conf
done
echo "}" >> /etc/nginx/conf.d/geo-blocking.conf
# Set proper permissions
chown root:root /etc/nginx/conf.d/security.conf /etc/nginx/conf.d/geo-blocking.conf
chmod 644 /etc/nginx/conf.d/security.conf /etc/nginx/conf.d/geo-blocking.conf
}
# Create example server configuration
create_example_config() {
echo -e "${BLUE}[5/6] Creating example server configuration...${NC}"
cat > /etc/nginx/conf.d/ddos-protection-example.conf << 'EOF'
server {
listen 80;
server_name example.com;
# Enable rate limiting logs
access_log /var/log/nginx/rate_limit.log rate_limit;
# Block based on geography
if ($blocked_country) {
return 444;
}
# Block suspicious user agents
if ($blocked_agent) {
return 444;
}
# Apply connection limits
limit_conn perip 10;
limit_conn perserver 100;
# General rate limiting
limit_req zone=general burst=5 nodelay;
# Login endpoint with stricter limits
location /login {
limit_req zone=login burst=2 nodelay;
# Your login handler here
return 200 "Login endpoint\n";
}
# API endpoints
location /api/ {
limit_req zone=api burst=20 nodelay;
# Your API handler here
return 200 "API endpoint\n";
}
# Default location
location / {
return 200 "Protected server\n";
}
}
EOF
chown root:root /etc/nginx/conf.d/ddos-protection-example.conf
chmod 644 /etc/nginx/conf.d/ddos-protection-example.conf
}
# Test and reload NGINX
test_and_reload() {
echo -e "${BLUE}[6/6] Testing configuration and reloading NGINX...${NC}"
# Test configuration
if ! nginx -t; then
echo -e "${RED}[ERROR] NGINX configuration test failed${NC}"
exit 1
fi
# Reload NGINX
systemctl reload nginx
echo -e "${GREEN}[SUCCESS] NGINX configuration reloaded${NC}"
}
# Verify installation
verify_installation() {
echo -e "${BLUE}[VERIFICATION] Checking installation...${NC}"
# Check if NGINX is running
if systemctl is-active --quiet nginx; then
echo -e "${GREEN}✓ NGINX is running${NC}"
else
echo -e "${RED}✗ NGINX is not running${NC}"
exit 1
fi
# Check if GeoIP database exists
if [[ -f /etc/nginx/geoip/GeoLite2-Country.mmdb ]]; then
echo -e "${GREEN}✓ GeoIP database installed${NC}"
else
echo -e "${RED}✗ GeoIP database missing${NC}"
fi
# Check configuration files
if [[ -f /etc/nginx/conf.d/security.conf ]]; then
echo -e "${GREEN}✓ Security configuration created${NC}"
else
echo -e "${RED}✗ Security configuration missing${NC}"
fi
echo -e "${GREEN}[SUCCESS] Installation completed successfully!${NC}"
echo -e "${YELLOW}[INFO] Example configuration created at: /etc/nginx/conf.d/ddos-protection-example.conf${NC}"
echo -e "${YELLOW}[INFO] Rate limit logs will be written to: /var/log/nginx/rate_limit.log${NC}"
echo -e "${YELLOW}[INFO] Blocked countries: $BLOCKED_COUNTRIES${NC}"
}
# Main function
main() {
check_privileges
detect_distro
install_nginx
verify_modules
setup_geoip
configure_nginx
create_example_config
test_and_reload
verify_installation
}
main "$@"
Review the script before running. Execute with: bash install.sh