Site icon GetPageSpeed

NGINX Honeypot 3.0: Advanced IP Blocking with nftables

NGINX Honeypot 3.0: Advanced IP Blocking with nftables

The NGINX Honeypot series reaches version 3.0 with a major change: replacing the legacy ipset backend with modern nftables. If you’ve used our NGINX Honeypot 2.0, this upgrade brings better performance and compatibility.

This NGINX honeypot auto-bans attackers, rate-limits abusers, and challenges bots with proof-of-work puzzles. All of this happens directly within NGINX itself.

Why Migrate from ipset to nftables?

The ipset utility served Linux administrators well for years. However, nftables has become the standard firewall framework. Here’s why migration matters for your NGINX honeypot:

Feature ipset (v2) nftables (v3)
RHEL 9/Rocky 9 compatibility Limited – firewalld uses nftables backend Full native support
Kernel integration Separate kernel module Built into nf_tables
firewalld compatibility iptables backend only nftables backend compatible
CIDR range support hash:net type Native interval sets
IPv4/IPv6 handling Separate sets required Auto-detection from client IP
Management commands ipset utility Unified nft utility

The ngx_http_nftset_access_module integrates directly with libnftables. It provides native kernel-level access control. No external processes or deprecated APIs are needed.

Key Features of the NGINX Honeypot Module

Core Access Control

Performance Optimizations

Security Features

Observability

Installation

RHEL, CentOS, AlmaLinux, Rocky Linux

sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-nftset-access

Enable the module in /etc/nginx/nginx.conf before the http {} block:

load_module modules/ngx_http_nftset_access_module.so;

Debian and Ubuntu

First, set up the GetPageSpeed APT repository, then install:

sudo apt-get update
sudo apt-get install nginx-module-nftset-access

On Debian/Ubuntu, the package handles module loading automatically. No load_module directive is needed.

See the RPM module page or APT module page for details.

Deployment Behind Proxies and Load Balancers

When NGINX sits behind a CDN or load balancer, client IPs arrive via headers. The module integrates with NGINX’s realip module to handle these scenarios.

Configuration Example

http {
    # Trust your load balancer/CDN to provide the real client IP
    set_real_ip_from 10.0.0.0/8;        # Internal load balancer network
    set_real_ip_from 172.16.0.0/12;     # Docker networks
    set_real_ip_from 192.168.0.0/16;    # Private networks
    set_real_ip_from 103.21.244.0/22;   # Cloudflare IPs (example)

    real_ip_header X-Real-IP;           # Or X-Forwarded-For
    real_ip_recursive on;

    server {
        listen 80;

        # Module uses the rewritten $remote_addr from realip
        nftset_blacklist filter:blacklist;

        # ...
    }
}

How It Works

  1. Request arrives from load balancer (e.g., 10.0.0.5)
  2. NGINX’s realip module extracts the client IP from the header
  3. The module checks the extracted IP against your nft sets
  4. Blocking decisions use the actual client IP, not the proxy IP

Logging with Both IPs

For debugging, log both the rewritten IP and the original:

log_format security '$remote_addr (proxy: $realip_remote_addr) '
                    '"$request" $status '
                    'nftset_result="$nftset_result"';

This produces logs like:

203.0.113.50 (proxy: 10.0.0.5) "GET / HTTP/1.1" 403 nftset_result="deny"

Important: Only trust set_real_ip_from addresses you control. Trusting arbitrary sources allows IP spoofing.

Quick Start: Creating nftables Sets

Before configuring your NGINX honeypot, create the nftables infrastructure:

# Create nftables table
sudo nft add table ip filter

# Create blacklist with timeout support (entries auto-expire)
sudo nft add set ip filter blacklist '{ type ipv4_addr; flags timeout; timeout 1d; }'

# Create honeypot set for trap URLs
sudo nft add set ip filter honeypot '{ type ipv4_addr; flags timeout; timeout 1d; }'

# Create rate-limit ban set
sudo nft add set ip filter ratelimited '{ type ipv4_addr; flags timeout; timeout 30m; }'

# Create whitelist with CIDR support (interval flag)
sudo nft add set ip filter trusted '{ type ipv4_addr; flags interval; }'
sudo nft add element ip filter trusted '{ 10.0.0.0/8, 192.168.0.0/16 }'

# For IPv6 support, create corresponding ip6 sets
sudo nft add table ip6 filter
sudo nft add set ip6 filter blacklist '{ type ipv6_addr; flags timeout; timeout 1d; }'

Basic NGINX Honeypot Configuration

Here’s a minimal setup that blocks bad IPs and creates honeypot traps:

load_module modules/ngx_http_nftset_access_module.so;

http {
    server {
        listen 80;
        server_name example.com;

        # Block IPs in blacklist (format: table:setname)
        nftset_blacklist filter:blacklist;

        # Return 403 Forbidden for blocked requests
        nftset_status 403;

        # Cache lookups for 60 seconds (default)
        nftset_cache_ttl 60s;

        location / {
            root /var/www/html;
        }

        # Honeypot trap - auto-add attackers and return 404
        location /wp-admin.php {
            nftset_autoadd filter:honeypot timeout=86400;
        }

        # Another trap with custom status code
        location /config.php {
            nftset_autoadd filter:honeypot timeout=3600 status=403;
        }
    }
}

Configuration Reference

All directives referencing nftables sets use the format: table:setname

The IP family (ip for IPv4, ip6 for IPv6) is auto-detected from the client’s address.

nftset_blacklist

Syntax: nftset_blacklist table:set1 [table:set2 ...] | off;
Context: http, server
Default:

Blocks requests if the client IP appears in any listed set. Sets are checked in order until a match is found.

# Single set
nftset_blacklist filter:bad_guys;

# Multiple sets (OR logic - blocked if in ANY set)
nftset_blacklist filter:spammers filter:hackers filter:tor_exits;

# Disable in specific server block
nftset_blacklist off;

nftset_whitelist

Syntax: nftset_whitelist table:set1 [table:set2 ...];
Context: http, server
Default:

Allows requests only if the client IP appears in at least one listed set. All other IPs are rejected.

# Only allow IPs in trusted sets
nftset_whitelist filter:trusted_partners filter:office_ips;

Important: Whitelisted IPs bypass all module restrictions. This includes rate limiting and JavaScript challenges.

nftset_status

Syntax: nftset_status code;
Context: http, server
Default: 403

HTTP status code returned when a request is blocked.

nftset_status 403;   # Forbidden (default)
nftset_status 444;   # Close connection without response (NGINX special)
nftset_status 429;   # Too Many Requests
nftset_status 503;   # Service Unavailable

nftset_dryrun

Syntax: nftset_dryrun on | off;
Context: http, server
Default: off

When enabled, logs what would be blocked but doesn’t block. This is perfect for testing new NGINX honeypot rules in production.

nftset_dryrun on;   # Log but don't block

Check logs for messages like: nftset: DRYRUN would block 1.2.3.4 (matched: filter:bad_guys)

nftset_cache_ttl

Syntax: nftset_cache_ttl time;
Context: http, server
Default: 60s

How long to cache nft set lookup results. Cached results avoid repeated kernel calls.

nftset_cache_ttl 30s;    # 30 seconds
nftset_cache_ttl 5m;     # 5 minutes
nftset_cache_ttl 1h;     # 1 hour

Performance tip: Higher TTL means better performance but slower reaction to changes. Use 30s to 5m for most deployments.

nftset_fail_open

Syntax: nftset_fail_open on | off;
Context: http, server
Default: off

Controls behavior when an nft set lookup fails.

nftset_fail_open off;   # Deny on error (secure, default)
nftset_fail_open on;    # Allow on error (available but risky)

nftset_ratelimit

Syntax: nftset_ratelimit rate=N [window=TIME] [autoban=TABLE:SET] [ban_time=N];
Context: http, server
Default:

Limits requests per IP within a time window. Can auto-add violators to an nft set.

Parameter Required Description
rate=N Yes Maximum requests per window
window=TIME No Time window (default: 60s)
autoban=TABLE:SET No nft set to add violators
ban_time=N No Seconds until auto-expire (default: 3600)
# Basic: 100 requests per minute
nftset_ratelimit rate=100;

# With custom window: 1000 requests per hour
nftset_ratelimit rate=1000 window=1h;

# With auto-ban: Add violators to nft set for 30 minutes
nftset_ratelimit rate=60 window=1m autoban=filter:ratelimited ban_time=1800;

# Strict API protection
nftset_ratelimit rate=10 window=1s autoban=filter:api_abusers ban_time=3600;

Rate-limited requests receive HTTP 429 Too Many Requests.

nftset_challenge

Syntax: nftset_challenge on | off;
Context: http, server
Default: off

Enables JavaScript challenge mode. Browsers must solve a proof-of-work puzzle. This blocks bots that cannot execute JavaScript.

nftset_challenge on;

How it works:

  1. First request receives a challenge page (HTTP 503)
  2. Browser executes JavaScript that solves a hash puzzle
  3. Solution is stored in a cookie (_nftset_verified)
  4. Subsequent requests with valid cookie pass through
  5. Cookie expires after 24 hours

nftset_challenge_difficulty

Syntax: nftset_challenge_difficulty level;
Context: http, server
Default: 2

Controls challenge difficulty (1-8). Higher values need more computation.

Level Approximate Solve Time
1 ~100ms
2 ~500ms (default)
3 ~1 second
4 ~2 seconds
5+ ~5-10+ seconds
nftset_challenge on;
nftset_challenge_difficulty 3;  # ~1 second solve time

nftset_autoadd

Syntax: nftset_autoadd table:setname [table:setname2 ...] [timeout=seconds] [status=code];
Context: server, location
Default:

Adds client IP to specified nft set(s) when accessed. This is the core directive for NGINX honeypot traps.

Parameter Required Description
table:setname Yes Target nft set (can specify multiple)
timeout=N No Entry timeout in seconds
status=N No HTTP status code to return (default: 404)
# Basic trap - add to set and return 404
location /config.php {
    nftset_autoadd filter:honeypot;
}

# With timeout - auto-expire after 24 hours
location /wp-admin.php {
    nftset_autoadd filter:scanners timeout=86400;
}

# Return 403 Forbidden instead of 404
location /admin.php {
    nftset_autoadd filter:honeypot timeout=86400 status=403;
}

nftset_stats

Syntax: nftset_stats;
Context: location
Default:

Enables the JSON statistics endpoint.

location = /_stats {
    nftset_stats;
    allow 127.0.0.1;
    deny all;
}

nftset_metrics

Syntax: nftset_metrics;
Context: location
Default:

Enables the Prometheus metrics endpoint.

location = /metrics {
    nftset_metrics;
    allow 127.0.0.1;
    deny all;
}

NGINX Variables

The module provides two variables for logging and conditionals:

$nftset_result

The access decision for this request.

Value Description
allow Request allowed
deny Request blocked
dryrun Would be blocked (dry-run mode)
ratelimited Rate limit exceeded
challenged Challenge page served

$nftset_matched_set

Name of the matched nft set in table:setname format. Empty if no match.

Usage Examples

Custom security log format:

log_format security '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    'nftset_result="$nftset_result" '
                    'matched_set="$nftset_matched_set"';

access_log /var/log/nginx/security.log security;

Conditional logging (only blocked requests):

map $nftset_result $loggable {
    "deny"       1;
    "ratelimited" 1;
    default      0;
}

access_log /var/log/nginx/blocked.log combined if=$loggable;

Observability

Prometheus Metrics

The /metrics endpoint returns Prometheus format:

# HELP nginx_nftset_requests_total Total requests processed
# TYPE nginx_nftset_requests_total counter
nginx_nftset_requests_total{result="checked"} 1234567
nginx_nftset_requests_total{result="allowed"} 1234000
nginx_nftset_requests_total{result="blocked"} 500
nginx_nftset_requests_total{result="error"} 67

# HELP nginx_nftset_cache_total Cache operations
# TYPE nginx_nftset_cache_total counter
nginx_nftset_cache_total{result="hit"} 1200000
nginx_nftset_cache_total{result="miss"} 34567

# HELP nginx_nftset_cache_entries Current cache entries
# TYPE nginx_nftset_cache_entries gauge
nginx_nftset_cache_entries 5432

# HELP nginx_nftset_ratelimit_total Rate limit events
# TYPE nginx_nftset_ratelimit_total counter
nginx_nftset_ratelimit_total{action="triggered"} 156
nginx_nftset_ratelimit_total{action="autobanned"} 23

# HELP nginx_nftset_challenge_total Challenge events
# TYPE nginx_nftset_challenge_total counter
nginx_nftset_challenge_total{result="issued"} 1000
nginx_nftset_challenge_total{result="passed"} 950
nginx_nftset_challenge_total{result="failed"} 50

Useful Grafana queries:

# Block rate per second
rate(nginx_nftset_requests_total{result="blocked"}[5m])

# Cache hit rate percentage
rate(nginx_nftset_cache_total{result="hit"}[5m]) /
(rate(nginx_nftset_cache_total{result="hit"}[5m]) + rate(nginx_nftset_cache_total{result="miss"}[5m])) * 100

# Rate limit violations per minute
rate(nginx_nftset_ratelimit_total{action="triggered"}[1m]) * 60

JSON Stats API

The /_stats endpoint returns detailed statistics:

{
  "version": "3.0.0",
  "uptime_seconds": 86400,
  "requests": {
    "checked": 1234567,
    "allowed": 1234000,
    "blocked": 500,
    "errors": 67
  },
  "cache": {
    "hits": 1200000,
    "misses": 34567,
    "entries": 5432,
    "hit_rate": 97.20
  },
  "autoadd": {
    "success": 42,
    "failed": 3
  },
  "ratelimit": {
    "triggered": 156,
    "autobanned": 23
  },
  "challenge": {
    "issued": 1000,
    "passed": 950,
    "failed": 50
  }
}

Complete NGINX Honeypot Example

This config implements layered security defenses:

load_module modules/ngx_http_nftset_access_module.so;

http {
    log_format security '$remote_addr - [$time_local] "$request" $status '
                        'result="$nftset_result" set="$nftset_matched_set"';

    server {
        listen 80 default_server;
        server_name example.com;

        access_log /var/log/nginx/security.log security;

        # Layer 1: Known threats
        nftset_blacklist filter:malware filter:tor_exits filter:datacenter;
        nftset_status 444;              # Silent close
        nftset_cache_ttl 5m;            # Aggressive caching

        # Layer 2: Rate limiting with auto-ban
        nftset_ratelimit rate=60 window=1m autoban=filter:ratelimited ban_time=1800;

        # Layer 3: Bot challenge (proof-of-work)
        nftset_challenge on;
        nftset_challenge_difficulty 2;

        # Real content
        location / {
            root /var/www/html;
        }

        # === NGINX HONEYPOT TRAPS ===

        # HIGH severity: Shell/exploit attempts (1 week ban)
        location ~ ^/(shell|cmd|eval|exec|backdoor)\.php$ {
            nftset_autoadd filter:honeypot timeout=604800 status=403;
        }

        # MEDIUM severity: CMS exploitation (24 hour ban)
        location ~ ^/(wp-admin|phpmyadmin|admin|manager)\.php$ {
            nftset_autoadd filter:honeypot timeout=86400;
        }

        # LOW severity: Config file probes (1 hour ban)
        location ~ ^/(\.env|config\.php|\.git|\.htaccess)$ {
            nftset_autoadd filter:honeypot timeout=3600 status=403;
        }

        # === MONITORING ===

        location = /_stats {
            nftset_stats;
            allow 127.0.0.1;
            allow 10.0.0.0/8;
            deny all;
        }

        location = /metrics {
            nftset_metrics;
            allow 127.0.0.1;
            allow 10.0.0.0/8;
            deny all;
        }
    }
}

Troubleshooting

Permission denied errors

NGINX worker needs CAP_NET_ADMIN capability:

sudo setcap cap_net_admin+ep /usr/sbin/nginx

SELinux denials (RHEL/Rocky/Alma)

The package includes an SELinux policy module. If you see denials:

# Check for SELinux denials
ausearch -m avc -ts recent | grep nginx

# The policy allows httpd_t to use netlink_netfilter sockets
semodule -l | grep nginx_nftset

Set not found errors

Ensure the nft table and set exist before starting NGINX:

# List available sets
nft list sets

# Create missing table and set
sudo nft add table ip filter
sudo nft add set ip filter myblacklist '{ type ipv4_addr; flags timeout; }'

Cache causing stale results

If the module reports a removed IP as matched, wait for cache TTL to expire. Set nftset_cache_ttl 0; for debugging.

autoadd fails with timeout error

The nft set lacks timeout support. Recreate it:

sudo nft delete set ip filter honeypot
sudo nft add set ip filter honeypot '{ type ipv4_addr; flags timeout; timeout 1d; }'

Migrating from NGINX Honeypot 2.0

Step 1: Convert ipsets to nft sets

# Old ipset command
ipset create bad_guys hash:ip timeout 86400

# New nft equivalent
nft add table ip filter
nft add set ip filter bad_guys '{ type ipv4_addr; flags timeout; timeout 1d; }'

Step 2: Update NGINX configuration

Old (ipset) New (nftset)
ipset_blacklist bad_guys; nftset_blacklist filter:bad_guys;
ipset_whitelist trusted; nftset_whitelist filter:trusted;
ipset_autoadd honeypot timeout=3600; nftset_autoadd filter:honeypot timeout=3600;
$ipset_result $nftset_result
$ipset_matched_set $nftset_matched_set

Step 3: Update monitoring

Replace nginx_ipset_* metrics with nginx_nftset_* in Grafana.

Conclusion

NGINX Honeypot 3.0 with ngx_http_nftset_access_module is a major evolution in server protection. Modern nftables replaces legacy ipset, giving you:

This module provides enterprise-grade security in a single package. It works for upgrades from v2 or new deployments.

Get the module from the GetPageSpeed Premium Repository. Visit the module page for docs or contact GetPageSpeed Support.

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Exit mobile version