Implement OpenResty rate limiting and DDoS protection with advanced Lua rules

Advanced 45 min Jun 13, 2026 551 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

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
sudo dnf update -y
sudo dnf install -y wget
wget https://openresty.org/package/centos/openresty.repo
sudo mv openresty.repo /etc/yum.repos.d/
sudo dnf install -y openresty redis lua-cjson

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 "======================

Automated install script

Run this to automate the entire setup

不想自己管理这些吗?

我们为依赖稳定运行时间的企业管理基础设施。全托管服务,配备一位熟悉您系统架构的固定联系人。

您将拥有一位了解您整体架构的固定联系人

Rotterdam 03:09 · 一条消息即可联系我们,无需填写工单表单