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
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
Rate Limit Exceeded
429 - Too Many Requests
You have exceeded the rate limit. Please wait a moment and try again.
If you believe this is an error, please contact support.
Access Forbidden
403 - Forbidden
Access to this resource is denied.
If you believe this is an error, please contact support.
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