Block known attackers in nginx with a free list that updates itself
Block known attackers in nginx with a geo map and a free list rebuilt every six hours: tested config, a safe update script, Cloudflare setup and real numbers.
To block known attackers in nginx, load their addresses into a geo map and return 403 to every visitor on it. Keep the list in its own file, refresh it on a timer, and reload nginx only after nginx -t passes. FFraud publishes a free list of 2,000 addresses seen attacking web applications, rebuilt every six hours. In our test, that list added 1.7 MB of memory to an nginx worker and 0.007 milliseconds to the median request.
#Which free list should you load?
FFraud publishes four plain-text lists with one address per line, strongest entries first. Each file's header states what qualifies an address. All four were rebuilt at 05:17 UTC on October 23, 2026, and FFraud rebuilds them every six hours.
| List | What it holds | IPs |
|---|---|---|
| webattack.txt | Seen attacking web apps, confirmed 4 or more times | 2,000 |
| badips.txt | Confirmed abusive 6 or more times in 14 days | 2,000 |
| worst.txt | Confirmed abusive 10 or more times in 14 days | 500 |
| freships.txt | First seen in the last 7 days, confirmed 3 or more times | 2,500 |
For a website or an API, start with
#Why use a geo map instead of deny lines?
nginx checks allow and deny rules in order until one matches. Its own documentation says that with many rules, the geo module is the better choice (nginx docs). A geo map sets a variable from the client address in a single lookup, and it can read its addresses from a file.
We measured what a list costs on nginx 1.24.0 with one worker process, on October 23, 2026. Each run loaded a list into a geo map and sent 2,000 requests from listed addresses and 2,000 from clean ones.
- No list4 MB
- webattack.txt, 2,000 IPs5.7 MB
- All four lists, 6,403 IPs10 MB
- Full open data, 1,556,296 IPs736.6 MB
Source: FFraud test on Ubuntu 24.04 (WSL); FFraud free lists (05:17 UTC build) and open data (09:30 UTC build), October 23, 2026
| List | nginx -t | Median request |
|---|---|---|
| No list | 0.008 s | 0.107 ms |
| 2,000 IPs | 0.006 s | 0.114 ms |
| 6,403 IPs | 0.008 s | 0.114 ms |
| 1,556,296 IPs | 1.886 s | 0.13 to 0.16 ms |
Lookups stayed fast at every size, and every listed address got 403 while every clean one got 200. Memory is the cost that grows. The full IPv4 part of FFraud's
#How do you set it up?
The steps below assume Ubuntu or Debian, where nginx reads every file in /etc/nginx/conf.d/ inside its http block. First, create an empty list so nginx can start before the first download:
sudo touch /etc/nginx/ffraud-blocklist.confNext, define the map. Every address in the list file gets the value 1, and everything else gets 0:
geo $ffraud_blocked {
default 0;
include /etc/nginx/ffraud-blocklist.conf;
}Then refuse listed visitors in each server block you want to protect:
server {
# your listen, server_name and location blocks stay as they are
if ($ffraud_blocked) {
return 403;
}
}The update script
Save this as /usr/local/bin/ffraud-nginx-blocklist and make it executable with chmod +x. It downloads the list, keeps only IPv4 addresses and ranges, removes your own addresses, and writes the map file:
#!/bin/sh
# Refresh nginx's list of known attackers from FFraud's free web-attacker feed.
# A failed download, a short list or a config that fails `nginx -t` leaves the old list in place.
set -eu
LIST_URL="https://ffraud.com/free/webattack.txt"
OUT=/etc/nginx/ffraud-blocklist.conf
ALLOW=/etc/nginx/ffraud-allowlist.txt # your own addresses, one per line (optional)
tmp=$(mktemp)
trap 'rm -f "$tmp" "$tmp.raw"' EXIT
curl -fsS --max-time 30 "$LIST_URL" -o "$tmp.raw"
# Keep IPv4 addresses and ranges only, drop your own addresses, write geo lines.
grep -E '^[0-9]{1,3}(\.[0-9]{1,3}){3}(/[0-9]{1,2})?$' "$tmp.raw" \
| if [ -s "$ALLOW" ]; then grep -v -x -F -f "$ALLOW"; else cat; fi \
| sed 's/$/ 1;/' > "$tmp" || true
n=$(wc -l < "$tmp")
if [ "$n" -lt 100 ]; then
echo "only $n addresses in the download, keeping the old list" >&2
exit 1
fi
[ -f "$OUT" ] && cp -p "$OUT" "$OUT.prev"
install -m 644 "$tmp" "$OUT"
if nginx -t -q; then
nginx -s reload
echo "loaded $n addresses"
else
echo "nginx -t failed, restoring the previous list" >&2
if [ -f "$OUT.prev" ]; then mv "$OUT.prev" "$OUT"; else : > "$OUT"; fi
exit 1
fiPut your own office, monitoring and partner addresses in /etc/nginx/ffraud-allowlist.txt, one per line, so a shared address of yours can never lock you out. The script refuses a download with fewer than 100 addresses, so an error page cannot replace a good list. If nginx -t fails for any reason, it restores the previous list and nginx keeps its running configuration.
A reload is safe for live traffic. nginx checks the new configuration first and keeps working with the old one when the check fails. When it passes, new workers start and the old ones finish their open requests (nginx docs).
Run it every six hours
A systemd timer matches the six-hour rebuild. Its random delay of up to ten minutes spreads the downloads of many servers over time (systemd docs):
# /etc/systemd/system/ffraud-nginx-blocklist.service
[Unit]
Description=Refresh the FFraud blocklist for nginx
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/ffraud-nginx-blocklist
# /etc/systemd/system/ffraud-nginx-blocklist.timer
[Unit]
Description=Refresh the FFraud blocklist for nginx every six hours
[Timer]
OnCalendar=*-*-* 00/6:40:00
RandomizedDelaySec=10m
Persistent=true
[Install]
WantedBy=timers.targetsudo /usr/local/bin/ffraud-nginx-blocklist
sudo systemctl daemon-reload
sudo systemctl enable --now ffraud-nginx-blocklist.timerWithout systemd, one cron line does the same: 40 */6 * * * root /usr/local/bin/ffraud-nginx-blocklist in /etc/cron.d/ffraud-nginx-blocklist.
#What if nginx sits behind Cloudflare or a load balancer?
Behind a proxy, nginx sees the proxy's address in place of the visitor's. A geo map then either blocks the proxy or never matches anyone. The realip module fixes this by replacing the client address with the one a trusted proxy sends (nginx docs).
Cloudflare puts the visitor's address in the CF-Connecting-IP header. It also asks you to keep its list of ranges current, because they change (Cloudflare docs):
# Trust Cloudflare's ranges and read the visitor from CF-Connecting-IP.
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
# ...one line for each range at https://www.cloudflare.com/ips-v4 and /ips-v6
real_ip_header CF-Connecting-IP;nginx applies the real address as soon as it reads the request, before the geo map is looked up. Our guide to X-Forwarded-For, and a script that keeps Cloudflare's ranges current.
#How do you see what got blocked?
Give blocked requests their own log. The if= parameter of access_log skips a request when the condition is 0 or empty (nginx docs). Add the format to the http block and the log line to each protected server block:
# http block
log_format ffraud_blocked '$time_iso8601 $remote_addr "$request" $status';
# server block, next to your normal access_log
access_log /var/log/nginx/blocked.log ffraud_blocked if=$ffraud_blocked;Each blocked request then appears as one line with the time, the visitor's address, the request and the 403. To check why an address is on the list, paste it into the free
#How did we test it?
We ran the script and the map on nginx 1.24.0 under Ubuntu 24.04 on October 23, 2026, on a private test instance. To send requests from listed addresses without spoofing packets, the test instance trusted 127.0.0.1 in realip and took the visitor from a header. Clean requests used the documentation range 198.51.100.0/24, which can never be on an attacker list (RFC 5737).
- The first run loaded 2,000 addresses. A listed address got 403, a clean one got 200, and the blocked request appeared in the log.
- With that listed address in the allowlist, the script loaded 1,999 addresses and the address got 200.
- When the download answered 404, the script stopped and the old list stayed in place.
- When the download was a web page and not a list, the script found 0 addresses and kept the old list.
- When the nginx configuration was broken on purpose,
nginx -tfailed, the script restored the previous list, and nginx kept serving.
On a production server, check the blocked log after the first few hours. A few lines from scanners and exploit attempts show that the map works.
#Should you return 403 or 444?
A 403 answers the request with Forbidden. Code 444 belongs to nginx alone: it closes the connection without sending a response header (nginx docs). A 444 saves a little traffic on scanners. A real person behind a listed shared address, such as a mobile carrier's
#Questions people ask
How do I block multiple IP addresses in nginx?
Put them in a file with one address 1; line each, include the file in a geo map, and return 403 when the map's variable is 1. nginx's docs prefer geo over long lists of deny lines.
Why is nginx letting blocked IP addresses through?
Most often because nginx sits behind a proxy and sees the proxy's address. Set up the realip module so the visitor's address is the one that gets checked. With allow and deny, also check the order: nginx stops at the first rule that matches.
How do I block IPs in nginx behind Cloudflare?
Trust Cloudflare's ranges with set_real_ip_from, read CF-Connecting-IP with real_ip_header, then block with the geo map as usual.
Can the geo module block a list of IPs instead of countries?
Yes. A geo map matches single addresses and ranges, so a list of attackers works the same way as country ranges.
How often should the list update?
FFraud rebuilds the free lists every six hours, so a timer every six hours picks up each build. Asking more often only fetches the same file.
For the complete list of confirmed attackers as a free download, see the
Sources
Module ngx_http_access_module Module ngx_http_geo_module Module ngx_http_realip_module Beginner's Guide: starting, stopping and reloading configuration Module ngx_http_log_module Module ngx_http_rewrite_module (return) Restoring original visitor IPs RFC 5737: IPv4 Address Blocks Reserved for Documentation Free web attacker list (webattack.txt), build of 05:17 UTC ip-fraud-database, build of 09:30 UTC