yum upgrades for production use, this is the repository for you.
Active subscription is required.
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
- Blacklist/Whitelist: Block or allow based on nftables set membership
- Multiple sets: Check against multiple nft sets in one directive
- Live updates: Modify nft sets without reloading NGINX
- Custom status codes: Return any HTTP status when blocking (403, 444, 429, etc.)
- CIDR support: Use interval sets for network ranges
Performance Optimizations
- Per-thread libnftables contexts: Thread-local sessions eliminate lock contention
- LRU Cache: Shared memory cache with configurable TTL achieves 95%+ hit rates
- Microsecond latency: Direct kernel queries via libnftables (50-100Ξs typical)
Security Features
- Rate limiting: Limit requests per IP with automatic ban for violators
- JavaScript challenge: Proof-of-work puzzle stops automated bots
- Honeypot traps: Auto-blacklist IPs hitting trap URLs
- Dry-run mode: Test rules in production without blocking
- Fail-open/close: Control behavior when nft operations fail
Observability
- Prometheus metrics: Native
/metricsendpoint for Grafana dashboards - JSON stats API: Detailed statistics for monitoring and alerting
- NGINX variables:
$nftset_resultand$nftset_matched_setfor logging
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
- Request arrives from load balancer (e.g.,
10.0.0.5) - NGINX’s
realipmodule extracts the client IP from the header - The module checks the extracted IP against your nft sets
- 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:
- First request receives a challenge page (HTTP 503)
- Browser executes JavaScript that solves a hash puzzle
- Solution is stored in a cookie (
_nftset_verified) - Subsequent requests with valid cookie pass through
- 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:
- Full compatibility with RHEL 9 and Rocky Linux 9
- Native firewalld integration with the nftables backend
- Unified management using the
nftcommand - Better performance via thread-local contexts and LRU caching
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.
