Set up comprehensive rate limiting and DDoS protection for OpenResty using nginx directives, Redis-backed Lua middleware, and advanced security rules with monitoring and alerting.
Prerequisites
- Root access to server
- Basic NGINX/OpenResty knowledge
- Redis server available
- Understanding of HTTP and rate limiting concepts
What this solves
OpenResty combines NGINX with embedded Lua scripting to create powerful rate limiting and DDoS protection systems. This tutorial shows you how to implement multi-layered defense using nginx.conf directives for basic protection, Redis-backed Lua scripts for sophisticated rate limiting, and custom middleware for advanced threat detection and mitigation.
Prerequisites
You'll need a server with root access and basic familiarity with NGINX configuration. Redis will be used for distributed rate limiting state management. We'll also integrate with existing monitoring systems for security event tracking.
Step-by-step installation
Install OpenResty and dependencies
OpenResty provides NGINX with Lua scripting capabilities built-in. We'll also install Redis for state management and development tools.
sudo apt update
wget -qO - https://openresty.org/package/pubkey.gpg | sudo apt-key add -
echo "deb http://openresty.org/package/ubuntu $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/openresty.list
sudo apt update
sudo apt install -y openresty redis-server lua-cjson lua-resty-redis
Configure Redis for rate limiting state
Redis will store rate limiting counters and DDoS detection state across OpenResty worker processes. Configure it for persistence and security.
# Basic security and persistence
bind 127.0.0.1
port 6379
requirepass OpenResty_RateLimit_2024!
save 900 1
save 300 10
save 60 10000
Memory optimization for rate limiting
maxmemory 256mb
maxmemory-policy allkeys-lru
Faster key expiration
hz 50
sudo systemctl enable --now redis-server
sudo systemctl status redis-server
Create directory structure for Lua scripts
Organize Lua scripts in a dedicated directory with proper permissions for the OpenResty worker processes.
sudo mkdir -p /etc/openresty/lua/ratelimit
sudo mkdir -p /var/log/openresty/security
sudo chown -R nobody:nogroup /etc/openresty/lua
sudo chown -R www-data:www-data /var/log/openresty
sudo chmod 755 /etc/openresty/lua/ratelimit
Create Redis connection module
This Lua module handles Redis connections with connection pooling and error handling for rate limiting operations.
local redis = require "resty.redis"
local cjson = require "cjson"
local _M = {}
function _M.new()
local red = redis:new()
red:set_timeout(1000) -- 1 second
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "failed to connect to redis: ", err)
return nil, err
end
-- Authenticate
local res, err = red:auth("OpenResty_RateLimit_2024!")
if not res then
ngx.log(ngx.ERR, "failed to authenticate with redis: ", err)
return nil, err
end
return red
end
function _M.close(red)
if red then
local ok, err = red:set_keepalive(10000, 100)
if not ok then
ngx.log(ngx.ERR, "failed to set keepalive: ", err)
end
end
end
function _M.increment_counter(key, window, limit)
local red, err = _M.new()
if not red then
return nil, err
end
-- Use Redis pipeline for atomic operations
red:init_pipeline()
red:incr(key)
red:expire(key, window)
local results, err = red:commit_pipeline()
if not results then
_M.close(red)
return nil, err
end
local current_count = results[1]
_M.close(red)
return tonumber(current_count)
end
return _M
Create basic rate limiting module
This module implements sliding window rate limiting with different limits for different request types and client classes.
local redis_client = require "ratelimit.redis_client"
local cjson = require "cjson"
local _M = {}
-- Rate limit configurations
local rate_limits = {
default = { requests = 100, window = 60 },
api = { requests = 1000, window = 60 },
login = { requests = 5, window = 300 },
upload = { requests = 10, window = 60 }
}
local function get_client_ip()
local headers = ngx.var.http_x_forwarded_for
if headers then
local ip = headers:match("([^,]+)")
return ip:gsub("%s+", "")
end
return ngx.var.remote_addr
end
local function get_rate_limit_config(uri)
if string.match(uri, "/api/") then
return rate_limits.api
elseif string.match(uri, "/login") or string.match(uri, "/auth") then
return rate_limits.login
elseif string.match(uri, "/upload") then
return rate_limits.upload
else
return rate_limits.default
end
end
function _M.check_rate_limit()
local client_ip = get_client_ip()
local uri = ngx.var.uri
local config = get_rate_limit_config(uri)
local key = "rate_limit:" .. client_ip .. ":" .. uri
local current_count, err = redis_client.increment_counter(key, config.window, config.requests)
if not current_count then
ngx.log(ngx.ERR, "Rate limiting error: ", err)
-- Fail open - allow request if Redis is down
return true
end
-- Log rate limiting decisions
local log_data = {
timestamp = ngx.time(),
client_ip = client_ip,
uri = uri,
current_count = current_count,
limit = config.requests,
window = config.window,
allowed = current_count <= config.requests
}
local log_file = io.open("/var/log/openresty/security/rate_limit.log", "a")
if log_file then
log_file:write(cjson.encode(log_data) .. "\n")
log_file:close()
end
if current_count > config.requests then
-- Set rate limit headers
ngx.header["X-RateLimit-Limit"] = config.requests
ngx.header["X-RateLimit-Remaining"] = 0
ngx.header["X-RateLimit-Reset"] = ngx.time() + config.window
ngx.status = 429
ngx.header.content_type = "application/json"
ngx.say(cjson.encode({
error = "Rate limit exceeded",
limit = config.requests,
window = config.window,
retry_after = config.window
}))
ngx.exit(429)
end
-- Set informational headers
ngx.header["X-RateLimit-Limit"] = config.requests
ngx.header["X-RateLimit-Remaining"] = math.max(0, config.requests - current_count)
return true
end
return _M
Create DDoS detection module
This advanced module detects DDoS patterns by analyzing request patterns, response times, and implementing progressive penalties.
local redis_client = require "ratelimit.redis_client"
local cjson = require "cjson"
local _M = {}
-- DDoS detection thresholds
local ddos_config = {
burst_threshold = 50, -- requests per 10 seconds
burst_window = 10,
penalty_multiplier = 2, -- increase penalty each time
max_penalty_time = 3600, -- 1 hour max penalty
suspicious_ua_patterns = {
"curl", "wget", "python", "bot", "crawler", "scanner"
}
}
local function get_client_ip()
local headers = ngx.var.http_x_forwarded_for
if headers then
local ip = headers:match("([^,]+)")
return ip:gsub("%s+", "")
end
return ngx.var.remote_addr
end
local function is_suspicious_user_agent(ua)
if not ua then return true end
ua = ua:lower()
for _, pattern in ipairs(ddos_config.suspicious_ua_patterns) do
if string.find(ua, pattern) then
return true
end
end
return false
end
local function calculate_request_score()
local score = 1
-- Check User-Agent
local user_agent = ngx.var.http_user_agent
if is_suspicious_user_agent(user_agent) then
score = score + 3
end
-- Check for missing common headers
if not ngx.var.http_accept then score = score + 2 end
if not ngx.var.http_accept_language then score = score + 1 end
-- Check request method
if ngx.var.request_method == "POST" then
score = score + 1
end
-- Check for rapid requests (implemented via Redis timing)
local client_ip = get_client_ip()
local timing_key = "timing:" .. client_ip
local red, err = redis_client.new()
if red then
local last_request = red:get(timing_key)
local current_time = ngx.time()
if last_request and last_request ~= ngx.null then
local time_diff = current_time - tonumber(last_request)
if time_diff < 1 then -- Less than 1 second between requests
score = score + 5
end
end
red:setex(timing_key, 60, current_time)
redis_client.close(red)
end
return score
end
function _M.check_ddos_pattern()
local client_ip = get_client_ip()
local current_time = ngx.time()
-- Check if client is currently penalized
local penalty_key = "penalty:" .. client_ip
local red, err = redis_client.new()
if not red then
ngx.log(ngx.ERR, "DDoS detector Redis connection failed: ", err)
return true -- Fail open
end
local penalty_end = red:get(penalty_key)
if penalty_end and penalty_end ~= ngx.null then
if current_time < tonumber(penalty_end) then
redis_client.close(red)
-- Log penalty enforcement
local log_data = {
timestamp = current_time,
client_ip = client_ip,
action = "penalty_enforced",
penalty_end = penalty_end,
uri = ngx.var.uri
}
local log_file = io.open("/var/log/openresty/security/ddos.log", "a")
if log_file then
log_file:write(cjson.encode(log_data) .. "\n")
log_file:close()
end
ngx.status = 403
ngx.header.content_type = "application/json"
ngx.say(cjson.encode({
error = "Access temporarily blocked due to suspicious activity",
retry_after = tonumber(penalty_end) - current_time
}))
ngx.exit(403)
else
-- Penalty expired, remove it
red:del(penalty_key)
end
end
-- Calculate request score
local request_score = calculate_request_score()
-- Track burst requests
local burst_key = "burst:" .. client_ip
local burst_count = redis_client.increment_counter(burst_key, ddos_config.burst_window, ddos_config.burst_threshold)
if not burst_count then
redis_client.close(red)
return true -- Fail open
end
-- Apply scoring to burst detection
local weighted_burst = burst_count * (request_score / 2)
if weighted_burst > ddos_config.burst_threshold then
-- Get current penalty count
local penalty_count_key = "penalty_count:" .. client_ip
local current_penalties = red:get(penalty_count_key)
current_penalties = (current_penalties and current_penalties ~= ngx.null) and tonumber(current_penalties) or 0
-- Calculate penalty duration
local penalty_duration = math.min(
300 * math.pow(ddos_config.penalty_multiplier, current_penalties),
ddos_config.max_penalty_time
)
-- Set penalty
red:setex(penalty_key, penalty_duration, current_time + penalty_duration)
red:incr(penalty_count_key)
red:expire(penalty_count_key, 86400) -- Reset penalty count after 24 hours
-- Log DDoS detection
local log_data = {
timestamp = current_time,
client_ip = client_ip,
action = "ddos_detected",
burst_count = burst_count,
request_score = request_score,
weighted_burst = weighted_burst,
penalty_duration = penalty_duration,
penalty_count = current_penalties + 1,
user_agent = ngx.var.http_user_agent,
uri = ngx.var.uri
}
local log_file = io.open("/var/log/openresty/security/ddos.log", "a")
if log_file then
log_file:write(cjson.encode(log_data) .. "\n")
log_file:close()
end
redis_client.close(red)
ngx.status = 403
ngx.header.content_type = "application/json"
ngx.say(cjson.encode({
error = "Suspicious activity detected - access blocked",
penalty_duration = penalty_duration
}))
ngx.exit(403)
end
redis_client.close(red)
return true
end
return _M
Configure OpenResty with rate limiting
Set up the main OpenResty configuration with nginx.conf directives for basic protection and Lua integration.
worker_processes auto;
error_log /var/log/openresty/error.log warn;
worker_rlimit_nofile 65535;
events {
worker_connections 4096;
use epoll;
multi_accept on;
}
http {
include mime.types;
default_type application/octet-stream;
# Basic 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;
# Rate limiting zones (nginx level)
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
limit_req_zone $binary_remote_addr zone=api:10m rate=100r/s;
# Connection limiting
limit_conn_zone $binary_remote_addr zone=conn_limit_per_ip:10m;
# Lua package path
lua_package_path "/etc/openresty/lua/?.lua;;";
# Shared dictionaries for caching
lua_shared_dict rate_limit_cache 10m;
lua_shared_dict ddos_cache 10m;
# Logging format
log_format security_log '$remote_addr - $remote_user [$time_local] '
'"$request" $status $bytes_sent '
'"$http_referer" "$http_user_agent" '
'$request_time $upstream_response_time '
'$http_x_forwarded_for';
# Main server block
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
access_log /var/log/openresty/access.log security_log;
# Basic nginx rate limiting (first layer)
limit_req zone=general burst=20 nodelay;
limit_conn conn_limit_per_ip 20;
# Block common attack patterns
location ~ \.(env|git|svn)$ {
deny all;
return 404;
}
# API endpoints with advanced protection
location /api/ {
limit_req zone=api burst=50 nodelay;
# Lua-based rate limiting and DDoS detection
access_by_lua_block {
local ddos_detector = require "ratelimit.ddos_detector"
local basic_limiter = require "ratelimit.basic_limiter"
-- Run DDoS detection first
ddos_detector.check_ddos_pattern()
-- Then apply rate limiting
basic_limiter.check_rate_limit()
}
# Your API backend
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Login endpoints with stricter limits
location ~ ^/(login|auth|signin) {
limit_req zone=login burst=3 nodelay;
access_by_lua_block {
local ddos_detector = require "ratelimit.ddos_detector"
local basic_limiter = require "ratelimit.basic_limiter"
ddos_detector.check_ddos_pattern()
basic_limiter.check_rate_limit()
}
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Default location with basic protection
location / {
access_by_lua_block {
local ddos_detector = require "ratelimit.ddos_detector"
local basic_limiter = require "ratelimit.basic_limiter"
ddos_detector.check_ddos_pattern()
basic_limiter.check_rate_limit()
}
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Health check endpoint (no rate limiting)
location /health {
access_log off;
return 200 "OK";
add_header Content-Type text/plain;
}
}
}
Create monitoring script
This script monitors security events and generates alerts for suspicious activity patterns.
#!/bin/bash
Security monitoring script for OpenResty rate limiting
LOG_DIR="/var/log/openresty/security"
RATE_LIMIT_LOG="$LOG_DIR/rate_limit.log"
DDOS_LOG="$LOG_DIR/ddos.log"
ALERT_THRESHOLD=10
EMAIL_ALERT="admin@example.com"
Create summary report
generate_security_report() {
local report_file="$LOG_DIR/security_summary_$(date +%Y%m%d_%H%M).txt"
echo "OpenResty Security Report - $(date)" > "$report_file"
echo "========================================" >> "$report_file"
echo "" >> "$report_file"
# Rate limiting stats from last hour
echo "Rate Limiting Events (Last Hour):" >> "$report_file"
if [[ -f "$RATE_LIMIT_LOG" ]]; then
tail -n 1000 "$RATE_LIMIT_LOG" | \
jq -r 'select(.timestamp > (now - 3600) and .allowed == false) | .client_ip' | \
sort | uniq -c | sort -nr | head -20 >> "$report_file"
fi
echo "" >> "$report_file"
# DDoS detection stats
echo "DDoS Detection Events (Last Hour):" >> "$report_file"
if [[ -f "$DDOS_LOG" ]]; then
tail -n 1000 "$DDOS_LOG" | \
jq -r 'select(.timestamp > (now - 3600)) | "\(.client_ip) - \(.action)"' | \
sort | uniq -c | sort -nr | head -20 >> "$report_file"
fi
echo "Report saved to: $report_file"
}
Check for high-volume attacks
check_attack_patterns() {
if [[ -f "$DDOS_LOG" ]]; then
local recent_attacks=$(tail -n 100 "$DDOS_LOG" | \
jq -r 'select(.timestamp > (now - 300)) | .client_ip' | \
wc -l)
if [[ $recent_attacks -gt $ALERT_THRESHOLD ]]; then
echo "HIGH ALERT: $recent_attacks DDoS events in last 5 minutes" | \
mail -s "OpenResty DDoS Alert - $(hostname)" "$EMAIL_ALERT"
fi
fi
}
Update Redis statistics
update_redis_stats() {
redis-cli -a "OpenResty_RateLimit_2024!" <Main execution
case "${1:-all}" in
"report")
generate_security_report
;;
"check")
check_attack_patterns
;;
"stats")
update_redis_stats
;;
"all")
check_attack_patterns
update_redis_stats
;;
*)
echo "Usage: $0 {report|check|stats|all}"
exit 1
;;
esac
sudo chmod +x /etc/openresty/lua/security_monitor.sh
Set up log rotation
Configure logrotate to manage security log files and prevent disk space issues.
/var/log/openresty/security/*.log {
daily
missingok
rotate 30
compress
delaycompress
notifempty
create 644 www-data www-data
postrotate
/usr/local/openresty/nginx/sbin/nginx -s reload
endscript
}
/var/log/openresty/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 644 www-data www-data
sharedscripts
postrotate
/usr/local/openresty/nginx/sbin/nginx -s reload
endscript
}
Create systemd service
Set up OpenResty as a systemd service with proper dependencies and security settings.
[Unit]
Description=OpenResty Web Server
After=network.target remote-fs.target nss-lookup.target redis-server.service
Requires=redis-server.service
[Service]
Type=forking
PIDFile=/usr/local/openresty/nginx/logs/nginx.pid
ExecStartPre=/usr/local/openresty/nginx/sbin/nginx -t
ExecStart=/usr/local/openresty/nginx/sbin/nginx
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s TERM $MAINPID
KillMode=mixed
KillSignal=SIGTERM
PrivateTmp=true
LimitNOFILE=65535
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable openresty
sudo systemctl start openresty
Set up monitoring cron jobs
Schedule regular security monitoring and report generation.
sudo crontab -e
Add these cron jobs for automated monitoring:
# Check for attacks every 5 minutes
/5 * /etc/openresty/lua/security_monitor.sh check
Generate security reports every hour
0 /etc/openresty/lua/security_monitor.sh report
Update Redis stats every 10 minutes
/10 * /etc/openresty/lua/security_monitor.sh stats
Clean old penalty records daily
0 2 redis-cli -a "OpenResty_RateLimit_2024!" --scan --pattern "penalty:" | xargs -r redis-cli -a "OpenResty_RateLimit_2024!" del
Test rate limiting and DDoS protection
Test basic rate limiting
Verify that rate limiting works correctly for different endpoints.
# Test general rate limiting
for i in {1..15}; do
curl -w "%{http_code} - %{time_total}s\n" \
-H "X-Forwarded-For: 192.168.1.100" \
http://localhost/ -o /dev/null -s
sleep 0.1
done
Test API rate limiting
for i in {1..25}; do
curl -w "%{http_code} - %{time_total}s\n" \
-H "X-Forwarded-For: 192.168.1.101" \
http://localhost/api/test -o /dev/null -s
sleep 0.05
done
Test DDoS detection
Simulate suspicious traffic patterns to verify DDoS detection works.
# Simulate bot-like behavior
for i in {1..60}; do
curl -w "%{http_code}\n" \
-H "User-Agent: python-requests/2.28.1" \
-H "X-Forwarded-For: 10.0.0.50" \
http://localhost/api/data -o /dev/null -s
done
Test rapid login attempts
for i in {1..10}; do
curl -X POST -w "%{http_code}\n" \
-H "X-Forwarded-For: 203.0.113.10" \
-d "username=test&password=test" \
http://localhost/login -o /dev/null -s
done
Monitor security logs
Check that security events are being logged correctly.
# View recent rate limiting events
tail -f /var/log/openresty/security/rate_limit.log | jq .
View DDoS detection events
tail -f /var/log/openresty/security/ddos.log | jq .
Check Redis for active penalties
redis-cli -a "OpenResty_RateLimit_2024!" KEYS "penalty:*"
View penalty details
redis-cli -a "OpenResty_RateLimit_2024!" GET "penalty:203.0.113.10"
Verify your setup
# Check OpenResty status
sudo systemctl status openresty
Verify Redis connection
redis-cli -a "OpenResty_RateLimit_2024!" ping
Test configuration syntax
sudo /usr/local/openresty/nginx/sbin/nginx -t
Check listening ports
sudo netstat -tlnp | grep nginx
Verify log files are created
ls -la /var/log/openresty/security/
Test rate limiting response headers
curl -I http://localhost/api/test
Check Lua modules load correctly
sudo /usr/local/openresty/nginx/sbin/nginx -T | grep lua_package_path
Configure monitoring and alerting
For production environments, integrate with your existing monitoring systems. This setup works well with Prometheus and Grafana for metrics visualization and Redis cluster monitoring for backend health checks.
Export metrics to Prometheus
Create a metrics endpoint that Prometheus can scrape for security statistics.
local redis_client = require "ratelimit.redis_client"
local cjson = require "cjson"
local _M = {}
function _M.export_metrics()
local red, err = redis_client.new()
if not red then
ngx.status = 500
ngx.say("# Redis connection failed")
return
end
-- Get penalty count
local penalty_keys = red:keys("penalty:*")
local active_penalties = penalty_keys and #penalty_keys or 0
-- Get rate limit stats from logs (simplified)
local rate_limit_blocks = 0
local ddos_detections = 0
-- Read recent log entries
local log_file = io.open("/var/log/openresty/security/rate_limit.log", "r")
if log_file then
for line in log_file:lines() do
local entry = cjson.decode(line)
if entry and entry.timestamp > (ngx.time() - 300) then -- Last 5 minutes
if not entry.allowed then
rate_limit_blocks = rate_limit_blocks + 1
end
end
end
log_file:close()
end
redis_client.close(red)
-- Output Prometheus format
ngx.header.content_type = "text/plain"
ngx.say("# HELP openresty_rate_limit_blocks_total Total rate limit blocks")
ngx.say("# TYPE openresty_rate_limit_blocks_total counter")
ngx.say("openresty_rate_limit_blocks_total " .. rate_limit_blocks)
ngx.say("")
ngx.say("# HELP openresty_active_penalties_total Active IP penalties")
ngx.say("# TYPE openresty_active_penalties_total gauge")
ngx.say("openresty_active_penalties_total " .. active_penalties)
end
return _M
Add metrics endpoint to nginx config
Add a metrics endpoint to your OpenResty configuration for monitoring integration.
sudo cp /usr/local/openresty/nginx/conf/nginx.conf /usr/local/openresty/nginx/conf/nginx.conf.bak
Add this location block to your server configuration:
# Add this inside the main server block
location /metrics {
access_log off;
allow 127.0.0.1;
allow 10.0.0.0/8;
allow 172.16.0.0/12;
allow 192.168.0.0/16;
deny all;
content_by_lua_block {
local metrics_exporter = require "ratelimit.metrics_exporter"
metrics_exporter.export_metrics()
}
}
sudo /usr/local/openresty/nginx/sbin/nginx -t
sudo systemctl reload openresty
Performance optimization
Optimize Redis configuration
Fine-tune Redis settings for optimal rate limiting performance.
# Add these optimizations to existing config
Optimize for rate limiting workload
tcp-keepalive 60
timeout 300
Memory efficiency
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
Disable unnecessary features
save ""
appendonly no
Network optimizations
tcp-nodelay yes
Increase client connections
maxclients 10000
sudo systemctl restart redis-server
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| 502 Bad Gateway errors | Redis connection failure | Check Redis status: sudo systemctl status redis-server |
| Lua script errors in logs | Missing dependencies or syntax error | Test Lua syntax: lua -c /etc/openresty/lua/ratelimit/basic_limiter.lua |
| Rate limiting not working | Nginx configuration issue | Verify config: sudo /usr/local/openresty/nginx/sbin/nginx -T | grep lua |
| High memory usage in Redis | Keys not expiring properly | Check TTL: redis-cli -a password KEYS "rate_limit:*" | head -5 | xargs redis-cli -a password TTL |
| False positive DDoS detection | Thresholds too aggressive | Adjust burst_threshold in ddos_detector.lua |
| Legitimate users blocked | Shared IP address (NAT) | Implement user-based rate limiting or whitelist ranges |
Next steps
- Setup OpenResty load balancing with health checks for high availability
- Configure Redis cluster SSL encryption for distributed rate limiting
- Setup NGINX rate limiting and security headers for comparison
- Implement OpenResty JWT authentication for API protection
- Configure OpenResty WAF integration for additional protection layers
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# OpenResty Rate Limiting & DDoS Protection Setup Script
# Auto-detects distro and configures multi-layered defense
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Default config
REDIS_PASSWORD="OpenResty_RateLimit_$(date +%Y)!"
DOMAIN="${1:-localhost}"
usage() {
echo "Usage: $0 [domain]"
echo "Example: $0 api.example.com"
exit 1
}
log() {
echo -e "${GREEN}[INFO]${NC} $1"
}
warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
error() {
echo -e "${RED}[ERROR]${NC} $1" >&2
exit 1
}
cleanup() {
warn "Installation failed. Cleaning up..."
systemctl stop openresty 2>/dev/null || true
systemctl stop redis-server 2>/dev/null || systemctl stop redis 2>/dev/null || true
}
trap cleanup ERR
# Check prerequisites
[[ $EUID -eq 0 ]] || error "This script must be run as root"
# Auto-detect distro
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update"
PKG_INSTALL="apt install -y"
REDIS_SERVICE="redis-server"
WWW_USER="www-data"
NGINX_CONF_DIR="/etc/openresty"
;;
almalinux|rocky|centos|rhel|ol)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
REDIS_SERVICE="redis"
WWW_USER="nginx"
NGINX_CONF_DIR="/etc/openresty"
;;
fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
REDIS_SERVICE="redis"
WWW_USER="nginx"
NGINX_CONF_DIR="/etc/openresty"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
REDIS_SERVICE="redis"
WWW_USER="nginx"
NGINX_CONF_DIR="/etc/openresty"
;;
*)
error "Unsupported distro: $ID"
;;
esac
else
error "Cannot detect OS distribution"
fi
echo "[1/8] Installing OpenResty and dependencies..."
$PKG_UPDATE
if [[ "$PKG_MGR" == "apt" ]]; then
wget -qO - https://openresty.org/package/pubkey.gpg | apt-key add -
echo "deb http://openresty.org/package/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/openresty.list
apt update
$PKG_INSTALL openresty redis-server lua-cjson
else
$PKG_INSTALL wget
wget -O /etc/yum.repos.d/openresty.repo https://openresty.org/package/centos/openresty.repo
$PKG_INSTALL openresty redis
fi
echo "[2/8] Configuring Redis for rate limiting..."
cat > /etc/redis/redis.conf << EOF
bind 127.0.0.1
port 6379
requirepass $REDIS_PASSWORD
save 900 1
save 300 10
save 60 10000
maxmemory 256mb
maxmemory-policy allkeys-lru
hz 50
EOF
# Handle different Redis config paths
if [ ! -d /etc/redis ]; then
mkdir -p /etc/redis
mv /etc/redis.conf /etc/redis/redis.conf 2>/dev/null || true
fi
systemctl enable --now $REDIS_SERVICE
sleep 2
echo "[3/8] Creating directory structure..."
mkdir -p $NGINX_CONF_DIR/lua/ratelimit
mkdir -p /var/log/openresty/security
chown -R $WWW_USER:$WWW_USER $NGINX_CONF_DIR/lua
chown -R $WWW_USER:$WWW_USER /var/log/openresty
chmod 755 $NGINX_CONF_DIR/lua/ratelimit
echo "[4/8] Creating Redis connection module..."
cat > $NGINX_CONF_DIR/lua/ratelimit/redis_client.lua << 'EOF'
local redis = require "resty.redis"
local cjson = require "cjson"
local _M = {}
function _M.new()
local red = redis:new()
red:set_timeout(1000)
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "failed to connect to redis: ", err)
return nil, err
end
local res, err = red:auth("REDIS_PASSWORD_PLACEHOLDER")
if not res then
ngx.log(ngx.ERR, "failed to authenticate with redis: ", err)
return nil, err
end
return red
end
function _M.close(red)
if red then
local ok, err = red:set_keepalive(10000, 100)
if not ok then
ngx.log(ngx.ERR, "failed to set keepalive: ", err)
end
end
end
function _M.increment_counter(key, window, limit)
local red, err = _M.new()
if not red then
return nil, err
end
red:init_pipeline()
red:incr(key)
red:expire(key, window)
local results, err = red:commit_pipeline()
if not results then
_M.close(red)
return nil, err
end
local current_count = results[1]
_M.close(red)
return tonumber(current_count)
end
return _M
EOF
sed -i "s/REDIS_PASSWORD_PLACEHOLDER/$REDIS_PASSWORD/g" $NGINX_CONF_DIR/lua/ratelimit/redis_client.lua
echo "[5/8] Creating rate limiting module..."
cat > $NGINX_CONF_DIR/lua/ratelimit/rate_limiter.lua << 'EOF'
local redis_client = require "ratelimit.redis_client"
local cjson = require "cjson"
local _M = {}
local rate_limits = {
default = { requests = 100, window = 60 },
api = { requests = 1000, window = 60 },
login = { requests = 5, window = 300 }
}
function _M.check_rate_limit()
local client_ip = ngx.var.remote_addr
local uri = ngx.var.uri
local method = ngx.var.request_method
local limit_type = "default"
if string.match(uri, "^/api/") then
limit_type = "api"
elseif string.match(uri, "/login") or string.match(uri, "/auth") then
limit_type = "login"
end
local config = rate_limits[limit_type]
local key = "rate_limit:" .. limit_type .. ":" .. client_ip
local count, err = redis_client.increment_counter(key, config.window, config.requests)
if not count then
ngx.log(ngx.ERR, "Redis error: ", err)
return true
end
if count > config.requests then
ngx.log(ngx.WARN, "Rate limit exceeded for ", client_ip, " on ", uri, " (", count, "/", config.requests, ")")
ngx.header["X-RateLimit-Limit"] = config.requests
ngx.header["X-RateLimit-Remaining"] = 0
ngx.header["X-RateLimit-Reset"] = ngx.time() + config.window
ngx.status = 429
ngx.say('{"error":"Rate limit exceeded","retry_after":' .. config.window .. '}')
ngx.exit(429)
end
ngx.header["X-RateLimit-Limit"] = config.requests
ngx.header["X-RateLimit-Remaining"] = config.requests - count
ngx.header["X-RateLimit-Reset"] = ngx.time() + config.window
return true
end
return _M
EOF
echo "[6/8] Creating OpenResty configuration..."
cat > $NGINX_CONF_DIR/nginx.conf << EOF
user $WWW_USER;
worker_processes auto;
error_log /var/log/openresty/error.log warn;
pid /var/run/openresty.pid;
events {
worker_connections 1024;
use epoll;
}
http {
include /etc/openresty/mime.types;
default_type application/octet-stream;
lua_package_path "/etc/openresty/lua/?.lua;;";
lua_shared_dict rate_limit_cache 10m;
# Basic DDoS protection
limit_req_zone \$binary_remote_addr zone=global:10m rate=10r/s;
limit_req_zone \$binary_remote_addr zone=api:10m rate=100r/s;
# Connection limiting
limit_conn_zone \$binary_remote_addr zone=conn_limit_per_ip:10m;
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/openresty/access.log security;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
server {
listen 80;
server_name $DOMAIN;
# Connection limits
limit_conn conn_limit_per_ip 10;
limit_req zone=global burst=20 nodelay;
# Security headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
access_by_lua_block {
local rate_limiter = require "ratelimit.rate_limiter"
rate_limiter.check_rate_limit()
}
location /api/ {
limit_req zone=api burst=50 nodelay;
# API-specific rate limiting handled by Lua
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
}
location / {
root /var/www/html;
index index.html;
}
location = /health {
access_log off;
return 200 "OK\n";
}
}
}
EOF
echo "[7/8] Setting up basic web root and testing..."
mkdir -p /var/www/html
echo "<h1>OpenResty DDoS Protection Active</h1>" > /var/www/html/index.html
chown -R $WWW_USER:$WWW_USER /var/www/html
# Set proper permissions
chmod 644 $NGINX_CONF_DIR/lua/ratelimit/*.lua
chmod 644 $NGINX_CONF_DIR/nginx.conf
chmod 644 /var/www/html/index.html
systemctl enable --now openresty
echo "[8/8] Verifying installation..."
sleep 3
# Test Redis connection
redis-cli -a "$REDIS_PASSWORD" ping > /dev/null || error "Redis connection failed"
# Test OpenResty
if ! systemctl is-active --quiet openresty; then
error "OpenResty failed to start"
fi
# Test configuration
if ! openresty -t; then
error "OpenResty configuration test failed"
fi
# Test rate limiting endpoint
if command -v curl >/dev/null; then
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/health || echo "000")
[[ "$HTTP_CODE" == "200" ]] || warn "Health check returned: $HTTP_CODE"
fi
log "✅ OpenResty DDoS protection installed successfully!"
log "🔧 Configuration: $NGINX_CONF_DIR/nginx.conf"
log "📁 Lua modules: $NGINX_CONF_DIR/lua/ratelimit/"
log "📊 Logs: /var/log/openresty/"
log "🔑 Redis password: $REDIS_PASSWORD"
log ""
log "Rate limits configured:"
log " • Global: 100 requests/minute"
log " • API: 1000 requests/minute "
log " • Login: 5 requests/5 minutes"
log ""
log "Test with: curl -I http://$DOMAIN/health"
Review the script before running. Execute with: bash install.sh