How to Block 200k Malicious IPs Automatically Using Gnoppix AI

Objective

On rbl.gnoppix.org (~182,000 IPs), we add all IP addresses that attempt to abuse our servers. This is a public RBL initially created for email, but you can also use it for other services, such as web-based applications.

As a modern approach, we use a Gnoppix-trained cybersecurity LLM to analyze traffic patterns. If the AI detects an abuse attempt, the IP address is added to the Gnoppix RBL cluster.

Deploy a secure NGINX setup on Gnoppix 26/27 that filters incoming traffic globally using a custom DNS Real-time Blackhole List (RBL) filter (rbl.gnoppix.org), correctly handles real visitor IP addresses behind Cloudflare, and integrates Redis to track page views and hit counts.

Step 1: Install Required Packages from the Gnoppix repo.

Update your package lists and install NGINX, the official NGINX Lua and Stream modules, Redis, and LuaRocks to manage Lua packages:

sudo apt update
sudo apt install -y nginx libnginx-mod-http-lua libnginx-mod-http-ndk libnginx-mod-stream redis-server luarocks

Verification: Run dpkg -l | grep nginx to confirm all required NGINX modules are installed.

Step 2: Install Lua Dependencies via LuaRocks

Install the necessary OpenResty Lua helper libraries for DNS resolution, Redis connection pooling, and string/caching utilities:

sudo luarocks install lua-resty-redis
sudo luarocks install lua-resty-dns
sudo luarocks install lua-resty-string
sudo luarocks install lua-resty-lrucache

Verification: Run luarocks list to verify that lua-resty-redis and lua-resty-dns are installed.

Step 3: Configure NGINX Modules

Open your primary NGINX configuration file:

sudo nano /etc/nginx/nginx.conf

Ensure the required dynamic modules are loaded at the very top, and define the lua_package_path inside the http {} block so NGINX can find the LuaRocks libraries:

Nginx

load_module modules/ndk_http_module.so;
load_module modules/ngx_http_lua_module.so;
load_module modules/ngx_stream_module.so;

events {
    worker_connections 1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    # Point to LuaRocks Lua package paths
    lua_package_path "/usr/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?.lua;;";

    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Verification: Save the file and run sudo nginx -t to ensure the syntax is correct.

Step 4: Configure the Virtual Host with Global RBL and Redis Hit Counter

Open your site-specific configuration file (e.g., /etc/nginx/sites-available/default or your domain configuration):

sudo nano /etc/nginx/sites-available/default

Replace or update the configuration with the complete script below. This places the RBL lookup globally in the server {} block and sets up the Redis counter under /redis:

Nginx

server {
    listen 80;
    server_name gnoppix.org localhost;

    # Global RBL check running on every request across the entire site
    access_by_lua_block {
        local resolver = require "resty.dns.resolver"
        local r, err = resolver:new{
            nameservers = {"1.1.1.1", "8.8.8.8"},
            retrans = 2,
            timeout = 1000
        }

        if not err then
            -- Fallback chain prioritizing Cloudflare's real visitor IP header
            local client_ip = ngx.var.http_cf_connecting_ip or ngx.var.http_x_forwarded_for or ngx.var.remote_addr
            if client_ip:find(",") then
                client_ip = client_ip:match("^%s*(.-)%s*,")
            end

            local function reverse_ip(ip)
                local o1, o2, o3, o4 = ip:match("(%d+)%.(%d+)%.(%d+)%.(%d+)")
                if o1 and o2 and o3 and o4 then
                    return string.format("%s.%s.%s.%s", o4, o3, o2, o1)
                end
                return nil
            end

            local reversed = reverse_ip(client_ip)
            if reversed then
                local rbl_zone = "rbl.gnoppix.org"
                local query_domain = reversed .. "." .. rbl_zone

                local answers, err = r:query(query_domain, { qtype = r.TYPE_A })

                if answers and not answers.errcode then
                    for _, ans in ipairs(answers) do
                        if ans.address then
                            ngx.log(ngx.ERR, "Blocked request from blacklisted IP: ", client_ip, " via ", rbl_zone)
                            ngx.status = ngx.HTTP_FORBIDDEN
                            ngx.header.content_type = "text/html; charset=utf-8"
                            ngx.say([[
                            <!DOCTYPE html>
                            <html lang="en">
                            <head>
                                <meta charset="UTF-8">
                                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                                <title>Access Denied - Security Check</title>
                                <style>
                                    body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background-color: #0f172a; color: #f8fafc; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }
                                    .container { background-color: #1e293b; padding: 2.5rem; border-radius: 12px; box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3); max-width: 450px; width: 100%; border-left: 5px solid #ef4444; }
                                    h1 { font-size: 1.5rem; margin-top: 0; color: #f1f5f9; }
                                    p { color: #94a3b8; line-height: 1.5; font-size: 0.95rem; }
                                    .meta { background: #0f172a; padding: 0.75rem 1rem; border-radius: 6px; font-family: monospace; font-size: 0.85rem; color: #cbd5e1; margin: 1.25rem 0; }
                                    .footer { font-size: 0.8rem; color: #64748b; margin-top: 1.5rem; }
                                </style>
                            </head>
                            <body>
                                <div class="container">
                                    <h1>Access Denied</h1>
                                    <p>Your request has been blocked by our security perimeter because your IP address matched a threat intelligence list.</p>
                                    <div class="meta">
                                        Client IP: ]] .. client_ip .. [[<br>
                                        Rule: rbl.gnoppix.org
                                    </div>
                                    <p>If you believe this is an error, please reach out to network support.</p>
                                    <div class="footer">Protected by Gnoppix Security Infrastructure</div>
                                </div>
                            </body>
                            </html>
                            ]])
                            return ngx.exit(ngx.HTTP_FORBIDDEN)
                        end
                    end
                end
            end
        end
    }

    # Location endpoint for testing Redis hit counting
    location /redis {
        default_type 'text/plain';

        content_by_lua_block {
            local redis = require "resty.redis"
            local red = redis:new()

            red:set_timeout(1000)
            local ok, err = red:connect("127.0.0.1", 6379)
            if not ok then
                ngx.say("failed to connect: ", err)
                return
            end

            local res, err = red:incr("hits")
            if not err then
                ngx.say("Page view count from Redis (Passed Gnoppix RBL): ", res)
            else
                ngx.say("failed to increment key: ", err)
            end

            red:set_keepalive(10000, 100)
        }
    }
}

Step 5: Test and Restart Services

Verify your entire NGINX configuration syntax, then enable and restart NGINX and Redis:

sudo nginx -t
sudo systemctl restart nginx
sudo systemctl enable --now redis-server

Verification: Run curl -i http://localhost/redis to verify that requests increment the Redis hit counter successfully.