yum upgrades for production use, this is the repository for you.
Active subscription is required.
The enterprise approach to IP blacklisting that eliminates shell scripts, sudo, and fcgiwrap
In a previous article, we explored how NGINX honeypots can catch and ban malicious bots by routing requests to a FastCGI script. That approach works—but it has inherent limitations that become painful at scale:
- Process overhead: Every honeypot hit spawns fcgiwrap → bash → ipset
- Attack surface: sudo-enabled shell scripts are a security liability
- Latency: Script execution adds milliseconds to what should be microseconds
- Complexity: fcgiwrap sockets, sudoers files, CGI scripts—too many moving parts
- No observability: How many bots are you catching? Which traps are most effective?
What if you could achieve the same result—and more—with a single NGINX directive?
The Solution: NGINX IPSet Access Module
The NGINX IPSet Access Module integrates libipset directly into NGINX. No external processes. No shell scripts. No sudo. Just native C code running at wire speed.
# Old way: 6 lines + external script + sudoers + fcgiwrap service
location /config.php {
fastcgi_intercept_errors off;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/local/libexec/block-ip.cgi;
fastcgi_pass unix:/run/fcgiwrap/fcgiwrap-nginx.sock;
keepalive_timeout 0;
}
# New way: 2 lines, zero dependencies
location /config.php {
ipset_autoadd honeypot;
return 200 "OK";
}
The difference isn’t just aesthetic. It’s architectural.
Prerequisites: Keep Your FirewallD Setup
The ipset infrastructure from the previous article remains unchanged. The module manipulates ipsets—it doesn’t replace them. If you haven’t set up your ipsets yet:
# Create ipsets (same as before)
firewall-cmd --permanent --new-ipset=honeypot4 --type=hash:ip --option=maxelem=1000000 --option=family=inet --option=hashsize=4096
firewall-cmd --permanent --new-ipset=honeypot6 --type=hash:ip --option=maxelem=1000000 --option=family=inet6 --option=hashsize=4096
firewall-cmd --permanent --zone=drop --add-source=ipset:honeypot4
firewall-cmd --permanent --zone=drop --add-source=ipset:honeypot6
firewall-cmd --reload
The ipsets work with FirewallD to drop all future connections at the kernel level. NGINX handles detection; the firewall handles enforcement.
Installing the Module
As our entire repository, the module is available for any RPM-based system and under both x86_64 and arm64 architectures.
# Install GetPageSpeed repository
sudo dnf -y install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-ipset-access
# Enable in nginx.conf (before http block)
load_module modules/ngx_http_ipset_access_module.so;
Grant NGINX the capability to modify ipsets:
sudo setcap cap_net_admin+ep /usr/sbin/nginx
Honeypot Configuration: The Modern Way
Here’s a complete honeypot configuration that replaces the entire fcgiwrap approach:
load_module modules/ngx_http_ipset_access_module.so;
http {
# Block anyone already in honeypot
ipset_blacklist honeypot4;
ipset_status 444; # Close connection silently
server {
listen 80;
server_name example.com;
# Your real content
location / {
root /var/www/html;
}
# === HONEYPOT TRAPS ===
# WordPress upload vulnerabilities (the original use case)
location ~ ^/wp-content/.*\.php$ {
ipset_autoadd honeypot4 timeout=86400;
return 200 "OK";
}
# WordPress config and install
location = /wp-config.php {
ipset_autoadd honeypot4 timeout=86400;
return 200 "OK";
}
location = /wp-admin/install.php {
ipset_autoadd honeypot4 timeout=86400;
return 200 "OK";
}
# Database admin tools that should never exist
location ~* ^/(phpmyadmin|pma|mysql|adminer)/ {
ipset_autoadd honeypot4 timeout=604800; # 1 week ban
return 200 "OK";
}
# Shell and exploit scanners get extra punishment
location ~ ^/(shell|cmd|backdoor|c99|r57|eval)\.(php|txt)$ {
ipset_autoadd honeypot4 timeout=2592000; # 30 day ban
return 200 "OK";
}
# Config file hunters
location ~ ^/\.(env|git|svn|htaccess|htpasswd) {
ipset_autoadd honeypot4 timeout=86400;
return 200 "OK";
}
}
}
That’s it. No CGI scripts. No sudoers configuration. No fcgiwrap service. When a bot hits any honeypot location, their IP is added to the ipset in microseconds, and their connection is terminated.
Beyond Honeypots: What Script-Based Solutions Can’t Do
The module doesn’t just simplify honeypots—it enables capabilities that would be impractical with external scripts.
Built-in Rate Limiting with Auto-Ban
Catch abusers automatically without log parsing:
# 100 requests/minute; violators banned for 30 minutes
ipset_ratelimit rate=100 window=60s autoban=ratelimited ban_time=1800;
This runs in shared memory. Every worker sees the same counters. No external daemons, no log tailing, no race conditions.
JavaScript Challenge for Bot Detection
Stop headless browsers and scripted attacks:
ipset_challenge on;
ipset_challenge_difficulty 2; # ~500ms solve time
First-time visitors must solve a proof-of-work puzzle. Real browsers handle it transparently. Automated scripts fail. No CAPTCHAs, no third-party services.
Host Header Validation (Built-in)
Remember the host header vulnerability protection from the previous article? The module makes it trivial:
# Catch requests with invalid Host headers
map $http_host $valid_host {
example.com 1;
www.example.com 1;
default 0;
}
server {
listen 80 default_server;
if ($valid_host = 0) {
# Invalid host → honeypot
set $trap 1;
}
location @honeypot {
ipset_autoadd honeypot4 timeout=86400;
return 444;
}
}
Production Observability
Know what’s happening in real-time:
location = /metrics {
ipset_metrics;
allow 10.0.0.0/8;
deny all;
}
location = /_stats {
ipset_stats;
allow 127.0.0.1;
deny all;
}
The /metrics endpoint exports Prometheus-compatible metrics:
nginx_ipset_requests_total{result="blocked"} 15234
nginx_ipset_autoadd_total{result="success"} 892
nginx_ipset_cache_total{result="hit"} 2847293
Build dashboards. Set alerts. Know exactly how many bots you’re catching and which honeypots are most effective.
Performance: Why It Matters at Scale
The old approach processes each honeypot hit like this:
Request → NGINX → FastCGI socket → fcgiwrap → bash → sudo → ipset CLI → response
That’s 5-15ms minimum. Under attack, you’re spawning hundreds of processes per second.
The module does this:
Request → NGINX (in-process libipset call) → response
That’s 50-100 microseconds. Two orders of magnitude faster. And the lookup cache means repeated checks for the same IP hit shared memory—not the kernel.
Cache Hit Rates
The module maintains an LRU cache of ipset lookups:
ipset_cache_ttl 60s; # Cache results for 60 seconds
In production, expect 95%+ cache hit rates. That means 95% of requests never touch the kernel for ipset queries.
Migration Checklist
If you’re running the fcgiwrap-based honeypot from the previous article, here’s your upgrade path:
- Install the module
sudo dnf install nginx-module-ipset-access - Update nginx.conf
- Add
load_module modules/ngx_http_ipset_access_module.so; - Add
ipset_blacklist honeypot4;in http or server block - Replace honeypot locations with
ipset_autoadddirectives
- Add
- Grant capability
sudo setcap cap_net_admin+ep /usr/sbin/nginx - Disable and remove old components
sudo systemctl disable --now fcgiwrap@nginx.socket sudo rm /etc/sudoers.d/nginx-block-ip sudo rm /usr/local/libexec/block-ip.cgi sudo rm /usr/local/sbin/block-ip.sh - Test and reload
sudo nginx -t && sudo systemctl reload nginx
Your honeypots now run faster, more securely, and with full observability.
Enterprise Deployment: Full Security Stack
Here’s a production configuration combining all security layers:
load_module modules/ngx_http_ipset_access_module.so;
http {
# === LAYER 1: Known Threats ===
ipset_blacklist honeypot4 malware_ips tor_exits;
ipset_status 444;
ipset_cache_ttl 5m;
ipset_fail_open off; # Deny on ipset errors (safe default)
# === LAYER 2: Rate Limiting ===
ipset_ratelimit rate=120 window=60s autoban=ratelimited ban_time=1800;
# === LAYER 3: Bot Challenge ===
ipset_challenge on;
ipset_challenge_difficulty 2;
# Custom logging for security analysis
log_format security '$remote_addr [$time_local] "$request" '
'$status ipset=$ipset_result matched=$ipset_matched_set';
access_log /var/log/nginx/security.log security;
server {
listen 80 default_server;
server_name _;
# Metrics (internal only)
location = /metrics {
ipset_metrics;
allow 10.0.0.0/8;
allow 127.0.0.1;
deny all;
}
# Real content
location / {
root /var/www/html;
index index.html;
}
# === HONEYPOT NETWORK ===
# Severity: HIGH - Shell/exploit attempts
location ~ \.(cgi|sh|pl)$ {
ipset_autoadd honeypot4 timeout=2592000;
return 200 "OK";
}
# Severity: MEDIUM - CMS exploitation
location ~ ^/wp-content/.*\.php$ {
ipset_autoadd honeypot4 timeout=604800;
return 200 "OK";
}
# Severity: MEDIUM - Admin tools
location ~* ^/(phpmyadmin|adminer|mysql)/ {
ipset_autoadd honeypot4 timeout=604800;
return 200 "OK";
}
# Severity: LOW - Generic probing
location ~ ^/\.(env|git|svn) {
ipset_autoadd honeypot4 timeout=86400;
return 200 "OK";
}
}
}
Testing in Production: Dry-Run Mode
Not ready to block? Test your configuration without affecting traffic:
ipset_blacklist suspicious_ips;
ipset_dryrun on;
Check logs for:
ipset: DRYRUN would block 203.0.113.42 (matched: suspicious_ips)
Tune your ipsets, verify your traps, then flip ipset_dryrun off; to go live.
The Bottom Line
| Aspect | fcgiwrap + Scripts | IPSet Access Module |
|---|---|---|
| Latency | 5-15ms | 50-100µs |
| Dependencies | fcgiwrap, bash, sudo | None |
| Security | Shell script attack surface | No shell, no sudo |
| Observability | DIY logging | Prometheus metrics |
| Rate Limiting | Requires Fail2ban | Built-in |
| Bot Challenge | Requires CAPTCHA service | Built-in JS PoW |
| Configuration | Multiple files | Single nginx.conf |
The honeypot concept hasn’t changed. The execution has evolved. What once required a stack of scripts and services is now a single directive in your NGINX configuration.
Your bots won’t notice the difference. Your infrastructure team will.
Ready to upgrade? The NGINX IPSet Access Module is available through the GetPageSpeed Premium Repository.
Stop feeding bots to scripts. Start feeding them to the firewall.
