Skip to content

abuse-guard: Auto-ban abusive clients by error-response rate (404/403/5xx)

Debian/Ubuntu installation

These docs apply to the APT package nginx-module-abuse-guard provided by the GetPageSpeed Extras repository.

  1. Configure the APT repository as described in APT repository setup.
  2. Install the module:
sudo apt-get update
sudo apt-get install nginx-module-abuse-guard
Show suites and architectures
| Distro   | Suite           | Component   | Architectures   |
|----------|-----------------|-------------|-----------------|
| debian   | trixie          | main        | amd64, arm64    |
| debian   | trixie-mainline | main        | amd64, arm64    |
| ubuntu   | noble           | main        | amd64, arm64    |
| ubuntu   | noble-mainline  | main        | amd64, arm64    |

Your error log is a confession. Abuse Guard reads it in real time and shuts abusers out.

Every scanner, fuzzer, and credential-stuffing bot leaves the same fingerprint: a spray of 404s hunting for hidden paths, 403s rattling locked doors, failed request after failed request. Abuse Guard watches the status codes your server actually returns, identifies the clients whose traffic is mostly failure, and locks them out — decided inside the NGINX worker, on the request itself, in a few microseconds. No sidecar. No log shipper. No scripting layer. Just compiled C doing one job exceptionally well.

Spec sheet

Trigger Per-client rate of error responses you choose (403/404 by default)
Action Timed lockout — a hard ban for a fixed window, not a throttle
Decision point NGINX preaccess phase, before any handler or upstream runs
Memory model Fixed bytes per client, independent of threshold → botnet-scale
Fleet mode Optional ban replication across nodes via Redis / Valkey
Durability Optional on-disk ban snapshots that survive reloads and reboots
Footprint One self-contained module; zero runtime dependencies by default
Platforms RHEL / AlmaLinux / Rocky / CentOS Stream / Oracle / Amazon Linux
##

The problem it removes

Legitimate visitors almost never generate a burst of errors. Abusers generate little else — that asymmetry is the whole game. A vulnerability scanner walking your tree is a wall of 404s. A bot poking admin endpoints is a wall of 403s. A brute-force run is a wall of failures.

Rate limiters treat that traffic like any other: they slow everyone by request volume and let the offender right back in the instant it eases off. Abuse Guard does the opposite. It ignores well-behaved traffic entirely and reserves its one response — a real, time-boxed ban — for clients defined by their errors.

Use a rate limiter to shape load. Use Abuse Guard to evict abuse.

How a ban is decided

Three moving parts, all inside the worker:

1 · A leaky score, per client. Every client identity carries a single small number in shared memory. Each matching error adds to it; the score bleeds away continuously at threshold ÷ interval per second. A short, sharp burst pushes it over the line; a slow trickle never does. Crucially, that score is one fixed-size record no matter how high you set the threshold — so a single zone comfortably tracks the tens of thousands of distinct source addresses a botnet throws at you.

2 · A hard deadline. The moment the score crosses your threshold, the client earns a blocked_until timestamp. Until then it is simply gone — every request turned away at the preaccess phase, before NGINX spends a cycle on routing, files, or upstreams. The rejection is the cheapest possible outcome.

3 · A privacy-correct refusal. Banned clients receive 429 Too Many Requests (your choice of code) tagged so no shared cache can ever store it and serve one client's punishment to another, with a Retry-After telling honest clients when to return.

Identities are folded into a fixed-size digest, so keying on something fat like $request_uri or a header costs exactly as much memory as keying on an IP.

Live in under a minute

Abuse Guard ships as a precompiled, signed module from the GetPageSpeed repository — drop it in, no build toolchain required.

sudo yum -y install https://extras.getpagespeed.com/release-latest.rpm
sudo yum -y install nginx-module-abuse-guard

Wire it up:

load_module modules/ngx_http_abuse_guard_module.so;

http {
    abuse_guard_zone zone=clients:10m;     # one shared-memory zone

    server {
        location / {
            abuse_guard zone=clients;      # enforce here
        }
    }
}
sudo nginx -t && sudo systemctl reload nginx

Those defaults ban any IP that returns 100 403/404 responses inside a 5-minute window, for one hour. Tighten or loosen every number below.

Configuration

Abuse Guard is four directives. The first declares a policy; the rest apply it, exempt people from it, and (optionally) share it across machines.

Declare a policy — abuse_guard_zone

An http-level directive. It carves out one shared-memory zone and sets the policy that governs it. Set as many or as few knobs as you like — the zone's name and size are the only thing you must provide; sensible defaults fill in the rest (the values shown below are exactly those defaults).

abuse_guard_zone  zone=clients:10m             name + size (the only must-have)
                  key=$binary_remote_addr      who is "one client"
                  statuses=403,404             which responses count as errors
                  interval=300s                the scoring window
                  threshold=100                errors in that window  ban
                  block=60m;                   how long the ban holds

zone=clients:10m is the policy's identity and budget: a name you reference from abuse_guard, and the shared-memory size. About 10 MB tracks on the order of a hundred thousand live clients.

Everything else is optional tuning:

  • key — the expression that defines a single client. Any NGINX variable; the default $binary_remote_addr keys on source IP. A request whose key comes out empty is skipped entirely (handy with a map, below).
  • statuses — the response codes that count as errors: individual codes, ranges, or a mix, e.g. statuses=401,403,404,500-599. Defaults to 403,404.
  • interval — the window the score decays over (default 300s). A burst inside it trips a ban; a slow trickle spread wider never accumulates.
  • threshold — how many errors within that window cross the line, up to 1024 (default 100).
  • block — how long a tripped client stays locked out (default 60m).
  • inactive — how long a dormant client lingers in memory before it's reclaimed (default max(1h, interval, block); any explicit value must be at least as large as both interval and block).
  • redison to replicate this zone's bans across a fleet (see below); off by default.
  • persist — a file path to snapshot bans into so they survive a restart.
  • persist_interval — how often that snapshot is rewritten (default 5s).
  • persist_secret — a hex key that signs the snapshot with HMAC-SHA256, so a tampered file is rejected rather than loaded.

Why 5xx is left out by default: a server error is usually your side's doing, and counting it would let one flaky backend get innocent visitors banned. Add statuses=403,404,500-599 only when you deliberately want to act on clients that trigger server errors.

Apply it — abuse_guard

Valid in http, server, and location blocks, so you can guard a whole site or just the endpoints that attract abuse. Name the zone to switch it on; write abuse_guard off; in a nested scope to switch it back off.

location /wp-login.php {
    abuse_guard zone=clients status=429 log_level=warn;
}
  • zone — the zone (declared above) whose policy applies here.
  • status — the code a banned client receives, anywhere in 400599 (default 429).
  • dry_runon to observe without enforcing: the verdict is logged but no ban is written. Off by default.
  • log_level — how loudly to log each decision: info, notice (default), warn, or error.

Roll out fearlessly with dry_run=on. It records every ban it would issue without touching state, so you can calibrate thresholds against live traffic — even next to an enforcing location on the same zone — then flip it live.

Exempt the good guys — abuse_guard_allow

Context: http · server · location · repeatable, inherited downward.

abuse_guard_allow 127.0.0.0/8;
abuse_guard_allow 10.0.0.0/8 192.168.0.0/16;

Listed clients are never counted and never banned. Matching is on the true connection address, so it cooperates with realip. This is also how you protect verified search crawlers: allow the published Googlebot / Bingbot ranges so a bot grinding through stale URLs (and racking up 404s) is never caught.

Share bans across the fleet — abuse_guard_redis

Context: http

abuse_guard_redis host=10.0.0.5 password=… ;   # tls://host for TLS
abuse_guard_zone  zone=clients:10m redis=on;

Point every node at one Redis or Valkey, flip redis=on, and a ban earned on any machine propagates to all of them. Defaults: port=6379, db=0, prefix=ag_, timeout=100ms. How it stays fast is the next section.

One ban, every node — without slowing a single request

Behind a load balancer, a per-server ban is theatre: the attacker just lands on a different node. Abuse Guard closes that gap without ever putting Redis in the request path.

Each node decides locally and counts locally. The instant it issues a ban, it broadcasts that one fact to the cluster and records a durable copy. Every other node imports it within milliseconds, and any node that was offline reconciles the moment it reconnects. Because enforcement is always served from each node's own in-memory state, a visitor's request never waits on a network round-trip — the only cost of clustering is that a freshly-banned attacker is shut out fleet-wide a heartbeat later instead of instantly.

Redis here is a one-way alarm bell, not a shared ledger consulted per request — so a slow or missing Redis can never add latency to your traffic. Run it on a private network and treat it as privileged: anything that can write to it can issue bans.

Bans that outlive a restart

Point a zone at a file and active bans are snapshotted on an interval and restored at startup, so a reload or a reboot doesn't hand every attacker a fresh slate.

abuse_guard_zone zone=clients:10m
                 persist=/var/lib/nginx/abuse_guard/clients.state
                 persist_secret=00112233445566778899aabbccddeeff;

The snapshot is integrity-checked, written so a crash can never leave a torn file, and — with persist_secret — cryptographically signed so a tampered file is rejected rather than trusted. Keep the directory readable only by the worker user.

See everything it decides

Three variables expose Abuse Guard's verdict to your logs and config:

Variable Value
$abuse_guard_status BYPASSED · PASSED · COUNTED · BLOCKED · DRY_RUN
$abuse_guard_count Errors currently attributed to this client.
$abuse_guard_blocked_until Unix time the ban lifts, or 0.
log_format guard '$remote_addr "$request" $status '
                 'guard=$abuse_guard_status count=$abuse_guard_count';

Keying behind a CDN or proxy? Never trust a raw X-Forwarded-For. Let realip resolve the real client first, then key on $binary_remote_addr:

set_real_ip_from 10.0.0.0/8;
real_ip_header   X-Forwarded-For;
real_ip_recursive on;

Need per-request exemption logic? Any request whose key resolves to an empty string is ignored — so a map lets you, say, track anonymous visitors by IP while leaving authenticated users untouched.

Engineered to be trusted in production

Abuse Guard is held to a standard far above "it compiles." Every change runs the gauntlet of AddressSanitizer, UndefinedBehaviorSanitizer, Valgrind, static analysis, and continuous fuzzing of its parsers and on-disk format. Its optional dependencies — clustering and signed snapshots — are best-effort by design: if Redis or the disk misbehaves, enforcement quietly carries on from local memory. Your traffic is never held hostage to a dependency.

Get Abuse Guard

Abuse Guard is a commercial NGINX module from GetPageSpeed LLC, delivered with ongoing updates and support through a GetPageSpeed subscription.

© GetPageSpeed LLC. All rights reserved.