📅 Updated: June 4, 2026 (Originally published: April 6, 2019)
Update (2026): this honeypot is now packaged as a one-line install — see Quick start below. If you’re building a new deployment, the kernel-ban (Pro) path skips fcgiwrap, CGI and sudo entirely; see also:
– NGINX Honeypot 2.0 – the deeper write-up of the IPSet Access module path
– NGINX Honeypot 3.0 – the modern nftset-access reference
The Internet is not a safe place these days. Hosting a public website means exposing it to multiple attacks from evil bots, which, at best will cause extra CPU and I/O load to your server.
If your web server is NGINX, you may be rightfully tempted to make use of some 3rd party WAF modules to counter the bad guys. One such module is nginx-module-security, other is NAXSI.
But what if I told you that there’s a trick that would allow your NGINX to easily filter out 99% of the bots out there, without third-party modules? Read on to find out how.
Quick start: dnf install nginx-honeypot
The hand-rolled article that lived here for years (FastCGI sockets, sudoers, bash scripts, the works) is now packaged. On any RHEL-family distro — EL7, EL8, EL9, EL10, Rocky, AlmaLinux, Amazon Linux 2, Amazon Linux 2023, Fedora 43/44, SUSE 16 — three commands and a one-line include get you a working honeypot:
sudo dnf -y install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf -y install nginx-honeypot
sudo ln -s /etc/nginx/honeypot/honey.conf /etc/nginx/conf.d/honey.conf
Then in each server { } block of your site:
include honeypot/server.conf;
nginx -t && systemctl reload nginx and you’re done. The package depends on nothing but nginx and ships pure-config — no daemons, no script chain to bootstrap, no firewall changes. Out of the box it runs in detection-only mode: bot-bait requests (the curated list of “honey” URIs in honey.conf) receive HTTP 410 Gone and your logs and app stay quiet. That’s already a big win.
Banning the offending IP is opt-in — see Pick your ban path below.
Know your average enemy (bot)
; tldr #1 – Evil bots try to upload
Suppose that you have a WordPress blog, and sure enough, the bad guys are trying to check if they are able to find a weak spot. They do this by trying different upload endpoints of various plugins. As an example, one of the bots was trying to access:
/wp-content/plugins/ungallery/source_vuln.php/wp-content/plugins/barclaycart/uploadify/uploadify.php/wp-content/plugins/barclaycart/uploadify/settings_auto.php/wp-content/plugins/hd-webplayer/playlist.php/wp-content/plugins/cherry-plugin/admin/import-export/upload.php/wp-content/plugins/viral-optins/api/uploader/file-uploader.php
Those plugins most likely do not even exist on your website!
So what we can obviously do, is ban any IP that attempts to access a resource that doesn’t exist on our site. Honeypot resources are either:
- locations that are known to not exist
- locations that aren’t supposed to be accessed by a genuine user, e.g. not linked from anywhere
- hostnames that are known to be invalid (see below)
The nginx-honeypot package’s honey.conf is a curated map of these bot-bait patterns — .env, /wp-includes/*.php, adminer, phpmyadmin, /actuator/health, /HNAP1, /remote/fgt_lang, the Exchange exporttool URL the bots love, and so on. The list is kept up to date as new bot fingerprints emerge — that’s the whole point of installing it from the repo instead of pasting locations into your vhost by hand.
What the package ships
After installation, /etc/nginx/honeypot/ contains:
honey.conf— themap $request_uri $has_fliesthat flags bot-bait URIs (symlink this once into/etc/nginx/conf.d/)server.conf— the default:if ($has_flies = 1) { return 410; }. Pure detection.server-ban-fcgiwrap.conf— free ban path (FastCGI → bash → firewall). Drop-in replacement forserver.conf.server-ban-nftset.conf— Pro ban path (kernel-level nftables vianftset_autoadd). Drop-in replacement forserver.conf.handler.conf— fcgiwrap FastCGI params (used internally by the free ban path)trusted-ips.conf— IP whitelist sourced by the ban script so you can’t lock yourself out
The package also drops:
/etc/sudoers.d/nginx-honeypot— pre-installed sudoers drop-in for the free path/usr/libexec/nginx-honeypot/block-ip.sh— backend-detecting bash ban script (legacy ipset / firewalld / raw nftables)/usr/libexec/nginx-honeypot/block-ip.cgi— the CGI handler invoked by fcgiwrap/usr/libexec/nginx-honeypot/setup-firewalld.sh— one-shot firewalld ipset bootstrap (auto-detects iptables vs nftables backend)/usr/libexec/nginx-honeypot/init-firewall.sh— raw-iptables bootstrap for hosts without firewalld
Pick your ban path
Detection alone is already useful — every 410 is a request your app didn’t have to serve. But you may want to also ban the offender, so the next time they probe you the kernel drops their packets before NGINX ever sees them. Two options ship in the box; include the file you want instead of server.conf:
Free: fcgiwrap + firewall
include honeypot/server-ban-fcgiwrap.conf;
A 410 hit is handed to a FastCGI CGI script that runs block-ip.sh (via the pre-installed sudoers drop-in) to add the offender to the honeypot4 / honeypot6 ipsets — and FirewallD’s drop zone takes care of the rest.
One-time setup:
sudo dnf install fcgiwrap ipset conntrack-tools
sudo /usr/libexec/nginx-honeypot/setup-firewalld.sh
sudo systemctl enable --now fcgiwrap@nginx.socket
To whitelist your office / admin / monitoring IPs, edit /etc/nginx/honeypot/trusted-ips.conf:
TRUSTED_IPS=("127.0.0.1" "::1" "203.0.113.7" "2001:db8::1")
Backend auto-detection (since nginx-honeypot 1.1.0): block-ip.sh picks the right add path on first call and caches it to /run/nginx-honeypot.backend:
- legacy ipset on EL7/8/9 (firewalld+iptables) and raw-iptables hosts — fast path, ~2 ms/ban
- firewalld via
firewall-cmd --ipset=…on EL10 (where firewalld defaults to the nftables backend and legacy/sbin/ipsetcan no longer see the set) — portable path, ~500 ms/ban - raw nftables when there’s no firewalld and you brought your own
ip filtertable + DROP rule
If you’re hosting at scale on EL10, prefer the Pro path below — sub-millisecond bans from inside NGINX itself, no shell-out, no firewall-cmd round-trip.
Pro: kernel-level ban with nftset-access (recommended for high-traffic sites)
include honeypot/server-ban-nftset.conf;
With the nginx-module-nftset-access module (GetPageSpeed NGINX Extras, Pro plan), the whole fcgiwrap / CGI / sudo chain collapses into a single directive — NGINX writes the offending IP straight into an nftables set and the kernel drops every subsequent packet in microseconds. The contents of server-ban-nftset.conf are simply:
error_page 410 = @honeypot;
if ($has_flies = 1) {
return 410;
}
location @honeypot {
nftset_autoadd filter:honeypot timeout=86400 status=410;
}
One-time setup:
sudo dnf install nginx-module-nftset-access
# Add to /etc/nginx/nginx.conf, BEFORE the events { } block:
# load_module modules/ngx_http_nftset_access_module.so;
sudo setcap cap_net_admin+ep /usr/sbin/nginx
# Create the nftables set + DROP rule (IPv4 shown; mirror with ip6 + ipv6_addr for v6):
sudo nft add table ip filter
sudo nft add set ip filter honeypot '{ type ipv4_addr; flags timeout; }'
sudo nft add chain ip filter input '{ type filter hook input priority 0; }'
sudo nft add rule ip filter input ip saddr @honeypot drop
No fcgiwrap, no CGI, no sudo, no shell script; the ban is applied in microseconds from inside NGINX, IPv4/IPv6 is auto-detected, and timeout=86400 auto-expires the entry after a day so your nftables set doesn’t grow unbounded.
For the full directive reference — rate limiting, JavaScript proof-of-work challenge, Prometheus metrics, dry-run mode, whitelist sets — see the dedicated write-up: NGINX Honeypot 3.0: Advanced IP Blocking with nftables.
Host header vulnerability protection
The honeypot approach is great when you want to protect your server from the hostname injection vulnerability.
The Host header vulnerability happens when a server or a website trusts the Host header in incoming internet requests without checking if it’s safe. This header is supposed to tell the server which website or page the visitor wants to see. But if bad guys change the Host header to something malicious, they can trick the server into sending them to a fake or dangerous place, steal information, or cause other security problems. It’s like sending a letter with the wrong address on purpose and then intercepting it to cause mischief.
; tldr #2 – Evil bots don’t even know your domain name!
Most of “current wave” bots will only know your IP, because they are scanning public IPv4 ranges and iterating one IP after another as their victim.
This is also when host header vulnerability takes place – they just put a random or intentionally incorrect Host header in their requests.
Those bots will share these common characteristics:
- Since all they know is your IP, they request resources with either an empty or fake
Hostheader - They will only make HTTP requests (not HTTPS)
So you can greatly reduce the load from those bots by blocking any client that does not provide valid hostnames. Obviously, valid hostnames are simply all domains that you host on your server, and any other domain would be an invalid hostname.
In /etc/nginx/nginx.conf, setup a map listing all your website domain names:
map $http_host $default_host_match {
getpagespeed.com 1;
www.getpagespeed.com 1;
default 0;
}
In a server block of your websites, route bad hostnames to the same honeypot the URI patterns use:
error_page 410 = @honeypot;
if ($default_host_match = 0) {
return 410;
}
The @honeypot named location is already declared by whichever server-ban-*.conf you included above, so the bad-Host requests get the same ban treatment as bad-URI requests for free.
Caveats
The honey map matches PHP under /wp-content/ — there is a slight chance you have a legitimate WordPress plugin that uses just this location to execute its PHP files. Such plugins should be reported and dealt with. But sure enough you don’t want to block valid users from your website.
To act out of extra precaution you may want to temporarily return 411; in this location and monitor your traffic with a script:
import os
import re
# grep 411 logs/access.log | grep wp-content > analyze.log
with open('analyze.log', 'r') as f:
log = f.read()
uris = re.findall(r'"(?:GET|POST) (?PS*)', log)
for uri in uris:
uri = uri.split('?')[0]
file_path = "httpdocs" + uri
print(file_path)
if os.path.exists(file_path):
print(file_path + " exists and requested!")
If the script returns no result, it means there are no actual PHP files in your WordPress plugins which are being accessed. If there are, those plugins should be removed or replaced. As a last resort, you can override the honey map — copy /etc/nginx/honeypot/honey.conf to your own location and remove the offending pattern, then symlink your version into /etc/nginx/conf.d/ instead of the shipped one.
How this compares to anything
Surely enough, you should not use this approach alone. There is never “enough security”, and you should use Fail2ban, Malware Detect, and ModSecurity.
However, we can see how the honeypot approach can complement the mentioned tools, and soften their disadvantages.
For example:
- Fail2ban continuously scans the log files in an attempt for early blocking of offenders; the honeypot banning matches immediately and reduces both the load and the log noise tremendously
- Malware Detect finds uploaded malware, whereas honeypot banning (and ModSecurity) prevent their upload in the first place
So other tools may be either slow or “too late”. They are surely enough useful. But now we can do better with the additional security layer thanks to the NGINX honeypot approach.
Prevent False Positives with Trusted Lists
One risk with honeypot blocking is accidentally banning legitimate services. Search engine crawlers like Googlebot might probe unexpected URLs, and payment processors send webhooks from IPs you don’t control.
To prevent false positives, combine the honeypot with trusted-lists — auto-updating FirewallD ipset packages that whitelist known-good services:
sudo dnf -y install firewalld-ipset-googlebot-v4 firewalld-ipset-paypal firewalld-ipset-stripe
sudo firewall-cmd --permanent --zone=trusted --add-source=ipset:googlebot-v4
sudo firewall-cmd --permanent --zone=trusted --add-source=ipset:paypal
sudo firewall-cmd --permanent --zone=trusted --add-source=ipset:stripe
sudo firewall-cmd --reload
The trusted zone takes priority over the drop zone, so whitelisted services can never get caught by the honeypot. See fds FirewallD Made Easy: Trusted Lists for the complete guide to combining blocking and whitelisting.
Manual install (under the hood)
This section is what the nginx-honeypot RPM automates. If you’d rather understand what’s wired up — or you’re on a non-RPM distro and need to build it by hand — here are the moving parts.
FirewallD ipsets (free ban path)
Two FirewallD IP sets, honeypot4 for IPv4 and honeypot6 for IPv6, both routed into the drop zone so any IP placed in them gets blocked at the firewall:
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 shipped /usr/libexec/nginx-honeypot/setup-firewalld.sh is exactly this script, made re-runnable, and prints a note when it detects the EL10 nftables backend so you know which add path block-ip.sh will pick.)
The honeypot include and the FastCGI handler
The shipped honeypot/handler.conf is what the free ban path passes 410 hits to:
fastcgi_intercept_errors off;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/libexec/nginx-honeypot/block-ip.cgi;
fastcgi_pass unix:/run/fcgiwrap/fcgiwrap-nginx.sock;
The CGI script itself (/usr/libexec/nginx-honeypot/block-ip.cgi) returns 410, closes the connection, and shells out to the ban script as root:
#!/usr/bin/bash
echo "Status: 410 Gone"
echo "Content-type: text/plain"
echo "Connection: close"
echo
echo "Bye bye, $REMOTE_ADDR!"
sudo /usr/libexec/nginx-honeypot/block-ip.sh
exit 0
The ban script
/usr/libexec/nginx-honeypot/block-ip.sh (since v1.1.0) sources trusted-ips.conf to whitelist your admin/office IPs, picks the active firewall backend (legacy ipset / firewalld / raw nftables), caches it to /run/nginx-honeypot.backend, then adds the remote IP via the right add path and tears down any existing TCP state with conntrack -D:
#!/usr/bin/bash
# Backends supported (auto-detected, cached in /run/nginx-honeypot.backend):
# 1. ipset - legacy kernel ipset (EL7/8/9 firewalld+iptables, raw iptables).
# Fast path (~2 ms).
# 2. firewalld - firewall-cmd --ipset=... (works on either FirewallBackend;
# required for EL10 where firewalld defaults to nftables and
# legacy /sbin/ipset no longer sees the set). ~500 ms.
# 3. nft - raw nftables, no firewalld. nft add element ip[6] filter ...
set -u
# ... trusted-IPs check, family detection (honeypot4 vs honeypot6) ...
detect_backend() {
if command -v ipset >/dev/null 2>&1 \
&& ipset list -n 2>/dev/null | grep -qx "$set_name"; then
echo "ipset"; return
fi
if command -v firewall-cmd >/dev/null 2>&1 \
&& firewall-cmd --get-ipsets 2>/dev/null | tr ' ' '\n' | grep -qx "$set_name"; then
echo "firewalld"; return
fi
if command -v nft >/dev/null 2>&1 \
&& nft list set "$nft_family" filter "$set_name" >/dev/null 2>&1; then
echo "nft"; return
fi
echo "none"
}
case "$backend" in
ipset) /sbin/ipset add "$set_name" "$REMOTE_ADDR" ;;
firewalld) firewall-cmd --ipset="$set_name" --add-entry="$REMOTE_ADDR" ;;
nft) nft add element "$nft_family" filter "$set_name" "{ $REMOTE_ADDR }" ;;
esac
The legacy ipset path stays the fast path (~2 ms/ban) where it works — that’s still the win on EL7/8/9. The firewalld path is what unblocks EL10 (firewalld stores its sets inside its own nft table, invisible to /sbin/ipset), at the cost of ~500 ms/ban on each cold add. The cache means subsequent bans skip the detection cost entirely.
sudoers — the nginx user needs to call block-ip.sh as root
The shipped /etc/sudoers.d/nginx-honeypot:
Defaults!/usr/libexec/nginx-honeypot/block-ip.sh env_keep=REMOTE_ADDR
nginx ALL=(ALL) NOPASSWD: /usr/libexec/nginx-honeypot/block-ip.sh
Keep-Alive
NGINX, like any other web server, supports keepalive connections. Simply blocking an IP in the firewall is not sufficient, because it affects only future connections. If bots are smart enough to use Keep-Alive, they can still make malicious requests over the initially established connection. That’s why the CGI sends Connection: close and the free ban path adds conntrack -D -s ${REMOTE_ADDR} — together they slam the door on both the application-layer connection and any in-flight TCP state. The Pro nftset_autoadd path also closes the existing connection naturally because the nftables drop rule applies to subsequent packets on the same flow.
Honeypot plus continuous monitoring. The trap blocks bots today. Continuous scanning catches the day a config edit removes the bait location. GetPageSpeed Amplify runs scheduled gixy scans across every host and ties findings to live NGINX runtime metrics. Drop-in compatible with the deprecated nginx-amplify-agent (EOL January 2026).

