<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Danila Vershinin</title>
    <description>The latest articles on DEV Community by Danila Vershinin (@dvershinin).</description>
    <link>https://dev.to/dvershinin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F397155%2F2120ef3e-9f76-4b0f-9a98-bd1ca8af90c3.jpeg</url>
      <title>DEV Community: Danila Vershinin</title>
      <link>https://dev.to/dvershinin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dvershinin"/>
    <language>en</language>
    <item>
      <title>NGINX Abuse Guard Module: Auto-Ban Scanners and Bots</title>
      <dc:creator>Danila Vershinin</dc:creator>
      <pubDate>Sun, 19 Jul 2026 17:06:05 +0000</pubDate>
      <link>https://dev.to/dvershinin/nginx-abuse-guard-module-auto-ban-scanners-and-bots-3a93</link>
      <guid>https://dev.to/dvershinin/nginx-abuse-guard-module-auto-ban-scanners-and-bots-3a93</guid>
      <description>&lt;p&gt;Open your NGINX error log on any public server and you will see the same thing: a steady drip of &lt;code&gt;404&lt;/code&gt;s probing for &lt;code&gt;/wp-login.php&lt;/code&gt;, &lt;code&gt;/.env&lt;/code&gt;, &lt;code&gt;/phpmyadmin&lt;/code&gt;, and a thousand other paths that do not exist on your site. A vulnerability scanner walking your tree is a wall of &lt;code&gt;404&lt;/code&gt;s. A bot rattling locked admin endpoints is a wall of &lt;code&gt;403&lt;/code&gt;s. A credential-stuffing run is a wall of failed logins. Legitimate visitors almost never generate a burst of errors, but abusers generate little else. That asymmetry is the entire signal the NGINX abuse guard module acts on, and most servers throw it away.&lt;/p&gt;

&lt;p&gt;The usual tools do not act on it cleanly. A rate limiter such as &lt;code&gt;limit_req&lt;/code&gt; shapes load by request volume, so it slows everyone during a flood and lets the offender back in the instant the rate eases. Tools like &lt;code&gt;fail2ban&lt;/code&gt; read the error pattern correctly, but they live outside NGINX: they tail a log file, parse it on a delay, and shell out to the firewall, so the ban lands seconds or minutes after the damage. What you actually want is to read the status codes your server is already returning and evict the clients whose traffic is mostly failure, immediately, inside the worker.&lt;/p&gt;

&lt;p&gt;That is exactly what the &lt;strong&gt;NGINX abuse guard module&lt;/strong&gt; does. It watches the final response status of every request, keeps a small per-client score in shared memory, and the moment a client crosses a threshold of errors it earns a timed ban enforced at the NGINX preaccess phase, before any handler, file lookup, or upstream runs. There is no sidecar, no log shipper, and no scripting layer. The decision happens in compiled C in a few microseconds. This guide shows you how it works, how to install and configure it, and how to roll it out safely against live traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the NGINX Abuse Guard Module Works
&lt;/h2&gt;

&lt;p&gt;The module reduces abuse detection to three moving parts, all of which live inside the NGINX worker. Understanding them makes every configuration choice below obvious.&lt;/p&gt;

&lt;h3&gt;
  
  
  A leaky, decaying score per client
&lt;/h3&gt;

&lt;p&gt;Every client identity carries a single small number in a shared-memory zone. Each matching error response adds to that score, and the score continuously bleeds away at a rate of &lt;code&gt;threshold ÷ interval&lt;/code&gt; per second. A short, sharp burst of errors pushes the score over the line and trips a ban; a slow trickle of the occasional &lt;code&gt;404&lt;/code&gt; spread across hours never accumulates. Crucially, that score is one fixed-size record no matter how high you set the threshold, so a single modest zone comfortably tracks the tens of thousands of distinct source addresses a botnet throws at you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Counting at the header filter, deciding at preaccess
&lt;/h3&gt;

&lt;p&gt;The module hooks two points in the request lifecycle. A header filter inspects the &lt;em&gt;final&lt;/em&gt; response status of each request and updates the offending client's score. Separately, the preaccess phase checks whether the current client is already banned. Because preaccess runs before routing, static-file handling, and upstream proxying, a banned client is turned away at the cheapest possible point: NGINX never spends a cycle serving the request it is about to reject.&lt;/p&gt;

&lt;h3&gt;
  
  
  A privacy-correct refusal
&lt;/h3&gt;

&lt;p&gt;When a banned client returns, it receives &lt;code&gt;429 Too Many Requests&lt;/code&gt; by default (you choose the code). The response carries a &lt;code&gt;Retry-After&lt;/code&gt; header telling honest clients when to come back, and a &lt;code&gt;Cache-Control: private, no-store&lt;/code&gt; header so that no shared cache or CDN can ever store one client's ban page and serve it to another. Identities are folded into a fixed-size digest before they are stored, so keying on something large like &lt;code&gt;$request_uri&lt;/code&gt; or a header costs exactly as much memory as keying on a plain IP address.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;Abuse Guard ships as a precompiled, signed dynamic module from the GetPageSpeed repository, so there is no build toolchain to install.&lt;/p&gt;

&lt;h3&gt;
  
  
  RHEL, CentOS, AlmaLinux, Rocky Linux, Amazon Linux
&lt;/h3&gt;

&lt;p&gt;Install the NGINX abuse guard module from the &lt;a href="https://nginx-extras.getpagespeed.com/modules/abuse-guard/" rel="noopener noreferrer"&gt;GetPageSpeed RPM repository&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install &lt;/span&gt;https://extras.getpagespeed.com/release-latest.rpm
&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install &lt;/span&gt;nginx-module-abuse-guard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then load the module by adding this line at the top of &lt;code&gt;/etc/nginx/nginx.conf&lt;/code&gt;, in the main context before any &lt;code&gt;http {}&lt;/code&gt; block:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;load_module modules/ngx_http_abuse_guard_module.so;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  Debian and Ubuntu
&lt;/h3&gt;

&lt;p&gt;First, &lt;a href="https://apt-nginx-extras.getpagespeed.com/apt-setup/" rel="noopener noreferrer"&gt;set up the GetPageSpeed APT repository&lt;/a&gt;, then install from the &lt;a href="https://apt-nginx-extras.getpagespeed.com/modules/abuse-guard/" rel="noopener noreferrer"&gt;APT module page&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install &lt;/span&gt;nginx-module-abuse-guard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;On Debian and Ubuntu, the package handles module loading automatically. No &lt;code&gt;load_module&lt;/code&gt; directive is needed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On either platform, confirm the configuration is valid before reloading:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo nginx -t
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;

&lt;p&gt;A working policy is two directives. Declare one shared-memory zone in the &lt;code&gt;http&lt;/code&gt; context, then switch enforcement on wherever abuse arrives:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;load_module&lt;/span&gt; &lt;span class="nc"&gt;modules/ngx&lt;/span&gt;&lt;span class="s"&gt;_http_abuse_guard_module.so&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;http&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;abuse_guard_zone&lt;/span&gt; &lt;span class="s"&gt;zone=clients:10m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;# one shared-memory zone&lt;/span&gt;

&lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kn"&gt;abuse_guard&lt;/span&gt; &lt;span class="s"&gt;zone=clients&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;# enforce here&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo nginx -t &amp;amp;&amp;amp; sudo systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;With nothing but a zone name and size, those defaults ban any single IP that returns &lt;strong&gt;100 &lt;code&gt;403&lt;/code&gt; or &lt;code&gt;404&lt;/code&gt; responses inside a 5-minute window, and the ban holds for one hour&lt;/strong&gt;. Every one of those numbers is tunable, as the reference below shows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Directive Reference
&lt;/h2&gt;

&lt;p&gt;The module is four directives: one declares a policy, the rest apply it, exempt trusted clients, and optionally share bans across machines. Everything documented here is verified against the module source, so you can rely on the contexts and defaults exactly as listed.&lt;/p&gt;

&lt;h3&gt;
  
  
  abuse_guard_zone
&lt;/h3&gt;

&lt;p&gt;Context: &lt;code&gt;http&lt;/code&gt;. Carves out one shared-memory zone and sets the policy that governs it. The zone name and size are the only required parameters; sensible defaults fill in the rest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;abuse_guard_zone&lt;/span&gt; &lt;span class="s"&gt;zone=clients:10m&lt;/span&gt;
                 &lt;span class="s"&gt;key=&lt;/span&gt;&lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt;
                 &lt;span class="s"&gt;statuses=403,404&lt;/span&gt;
                 &lt;span class="s"&gt;interval=300s&lt;/span&gt;
                 &lt;span class="s"&gt;threshold=100&lt;/span&gt;
                 &lt;span class="s"&gt;block=60m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zone&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(required)&lt;/td&gt;
&lt;td&gt;The policy name (referenced by &lt;code&gt;abuse_guard&lt;/code&gt;) and the shared-memory size, e.g. &lt;code&gt;clients:10m&lt;/code&gt;. About 10 MB tracks on the order of a hundred thousand live clients.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$binary_remote_addr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The expression that defines one client. Any NGINX variable. A request whose key resolves to an empty string is skipped entirely.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;statuses&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;403,404&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which response codes count as errors. Individual codes, ranges, or a mix, e.g. &lt;code&gt;statuses=401,403,404,500-599&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;interval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;300s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The window the score decays over. A burst inside it trips a ban; a slow trickle never accumulates.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;threshold&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;100&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How many errors within the window cross the line, up to 1024.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;block&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;60m&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How long a tripped client stays locked out.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;inactive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;max(1h, interval, block)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How long a dormant client lingers in memory before reclamation. Any explicit value must be at least as large as both &lt;code&gt;interval&lt;/code&gt; and &lt;code&gt;block&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redis&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;off&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;on&lt;/code&gt; to replicate this zone's bans across a fleet (see below).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;persist&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(none)&lt;/td&gt;
&lt;td&gt;A file path to snapshot bans into so they survive a restart.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;persist_interval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How often the snapshot is rewritten.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;persist_secret&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(none)&lt;/td&gt;
&lt;td&gt;A hex key that signs the snapshot with HMAC-SHA256, so a tampered file is rejected rather than loaded.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;h3&gt;
  
  
  abuse_guard
&lt;/h3&gt;

&lt;p&gt;Context: &lt;code&gt;http&lt;/code&gt;, &lt;code&gt;server&lt;/code&gt;, &lt;code&gt;location&lt;/code&gt;. Applies a declared policy. Name the zone to switch enforcement on; write &lt;code&gt;abuse_guard off;&lt;/code&gt; in a nested scope to switch it back off for that scope.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/wp-login.php&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;abuse_guard&lt;/span&gt; &lt;span class="s"&gt;zone=clients&lt;/span&gt; &lt;span class="s"&gt;status=429&lt;/span&gt; &lt;span class="s"&gt;log_level=warn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zone&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(required)&lt;/td&gt;
&lt;td&gt;The zone (declared with &lt;code&gt;abuse_guard_zone&lt;/code&gt;) whose policy applies here.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;429&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The code a banned client receives, anywhere in &lt;code&gt;400&lt;/code&gt; to &lt;code&gt;599&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dry_run&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;off&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;on&lt;/code&gt; to observe without enforcing: the verdict is logged but no ban is written.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;log_level&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;notice&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How loudly to log each decision: &lt;code&gt;info&lt;/code&gt;, &lt;code&gt;notice&lt;/code&gt;, &lt;code&gt;warn&lt;/code&gt;, or &lt;code&gt;error&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  abuse_guard_allow
&lt;/h3&gt;

&lt;p&gt;Context: &lt;code&gt;http&lt;/code&gt;, &lt;code&gt;server&lt;/code&gt;, &lt;code&gt;location&lt;/code&gt;. Repeatable and inherited downward. Listed clients are never counted and never banned. Matching is on the true connection address, so it cooperates with &lt;code&gt;realip&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;abuse_guard_allow&lt;/span&gt; &lt;span class="mf"&gt;127.0&lt;/span&gt;&lt;span class="s"&gt;.0.0/8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;abuse_guard_allow&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="s"&gt;.0.0/8&lt;/span&gt; &lt;span class="mf"&gt;192.168&lt;/span&gt;&lt;span class="s"&gt;.0.0/16&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  abuse_guard_redis
&lt;/h3&gt;

&lt;p&gt;Context: &lt;code&gt;http&lt;/code&gt;. Points the server at one Redis or Valkey instance for fleet-wide ban replication. Pair it with &lt;code&gt;redis=on&lt;/code&gt; on the zone you want shared.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;abuse_guard_redis&lt;/span&gt; &lt;span class="s"&gt;host=10.0.0.5&lt;/span&gt; &lt;span class="s"&gt;password=secret&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;# tls://host for TLS&lt;/span&gt;
&lt;span class="k"&gt;abuse_guard_zone&lt;/span&gt;  &lt;span class="s"&gt;zone=clients:10m&lt;/span&gt; &lt;span class="s"&gt;redis=on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;host&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(required)&lt;/td&gt;
&lt;td&gt;The Redis or Valkey host. Prefix with &lt;code&gt;tls://&lt;/code&gt; to connect over TLS.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;password&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(none)&lt;/td&gt;
&lt;td&gt;Authentication password, if the server requires one.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;port&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;6379&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The server port.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;db&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The logical database number.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;prefix&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ag_&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Key and channel prefix, so several services can share one Redis safely.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;timeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;100ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Connection and command timeout for the out-of-band replication path.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Variables
&lt;/h2&gt;

&lt;p&gt;The module exposes exactly three variables, which you can write to your access log or use in a &lt;code&gt;map&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$abuse_guard_status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The verdict for this request: &lt;code&gt;BYPASSED&lt;/code&gt;, &lt;code&gt;PASSED&lt;/code&gt;, &lt;code&gt;COUNTED&lt;/code&gt;, &lt;code&gt;BLOCKED&lt;/code&gt;, or &lt;code&gt;DRY_RUN&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$abuse_guard_count&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The error count currently attributed to this client.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$abuse_guard_blocked_until&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The Unix timestamp when the ban lifts, or &lt;code&gt;0&lt;/code&gt; if the client is not banned.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A custom log format makes the module's decisions visible while you tune it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;log_format&lt;/span&gt; &lt;span class="s"&gt;guard&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="nv"&gt;$remote_addr&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="nv"&gt;$status&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;
                 &lt;span class="s"&gt;'guard=&lt;/span&gt;&lt;span class="nv"&gt;$abuse_guard_status&lt;/span&gt; &lt;span class="s"&gt;count=&lt;/span&gt;&lt;span class="nv"&gt;$abuse_guard_count&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;access_log&lt;/span&gt; &lt;span class="n"&gt;/var/log/nginx/access.log&lt;/span&gt; &lt;span class="s"&gt;guard&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Security Hardening Use Cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Blocking vulnerability scanners and 404 floods
&lt;/h3&gt;

&lt;p&gt;This is the case the defaults are built for. An automated scanner requests hundreds of nonexistent paths in quick succession, and every miss is a &lt;code&gt;404&lt;/code&gt;. Enforce the default policy site-wide and the scanner bans itself within seconds of starting, while a real visitor who fat-fingers one URL never comes close to the threshold:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;http&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;abuse_guard_zone&lt;/span&gt; &lt;span class="s"&gt;zone=scanners:10m&lt;/span&gt; &lt;span class="s"&gt;statuses=404&lt;/span&gt; &lt;span class="s"&gt;threshold=40&lt;/span&gt; &lt;span class="s"&gt;interval=60s&lt;/span&gt; &lt;span class="s"&gt;block=30m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kn"&gt;abuse_guard&lt;/span&gt; &lt;span class="s"&gt;zone=scanners&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Brute-force protection on login and admin endpoints
&lt;/h3&gt;

&lt;p&gt;Credential-stuffing and admin-probing bots produce &lt;code&gt;403&lt;/code&gt;s and failed logins. Scope a stricter policy to the endpoints they target, so a handful of failures is tolerated but a sustained run is locked out, without affecting the rest of the site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;/wp-login.php&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;abuse_guard&lt;/span&gt; &lt;span class="s"&gt;zone=clients&lt;/span&gt; &lt;span class="s"&gt;status=429&lt;/span&gt; &lt;span class="s"&gt;log_level=warn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;# ... your usual fastcgi_pass / proxy_pass ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Exempting authenticated users with a map
&lt;/h3&gt;

&lt;p&gt;Because a request whose &lt;code&gt;key&lt;/code&gt; resolves to an empty string is skipped, a &lt;code&gt;map&lt;/code&gt; lets you track anonymous visitors by IP while leaving logged-in users untouched. Here, requests that carry a session cookie produce an empty key and are never scored:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;map&lt;/span&gt; &lt;span class="nv"&gt;$cookie_sessionid&lt;/span&gt; &lt;span class="nv"&gt;$abuse_key&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;""&lt;/span&gt;      &lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;# anonymous: key on IP&lt;/span&gt;
    &lt;span class="kn"&gt;default&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                    &lt;span class="c1"&gt;# logged in: skip entirely&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;http&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;abuse_guard_zone&lt;/span&gt; &lt;span class="s"&gt;zone=clients:10m&lt;/span&gt; &lt;span class="s"&gt;key=&lt;/span&gt;&lt;span class="nv"&gt;$abuse_key&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Protecting verified search crawlers
&lt;/h3&gt;

&lt;p&gt;A search-engine crawler grinding through stale URLs can rack up &lt;code&gt;404&lt;/code&gt;s through no fault of yours. Allowlist the published crawler ranges so they are never counted. Because matching is on the real connection address, this also cooperates with &lt;code&gt;realip&lt;/code&gt; when you run behind a CDN:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;abuse_guard_allow&lt;/span&gt; &lt;span class="mf"&gt;66.249&lt;/span&gt;&lt;span class="s"&gt;.64.0/19&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;# example Googlebot range&lt;/span&gt;
&lt;span class="k"&gt;abuse_guard_allow&lt;/span&gt; &lt;span class="mf"&gt;157.55&lt;/span&gt;&lt;span class="s"&gt;.0.0/16&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;# example Bingbot range&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Rolling out safely with dry_run
&lt;/h3&gt;

&lt;p&gt;Never flip a new ban policy straight to enforcing on production traffic. Run it in observation mode first: &lt;code&gt;dry_run=on&lt;/code&gt; records every ban it &lt;em&gt;would&lt;/em&gt; issue, in the log, without writing any state. You can even run a dry-run location next to an enforcing one on the same zone, calibrate the threshold against real traffic, then flip it live once the numbers look right:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;abuse_guard&lt;/span&gt; &lt;span class="s"&gt;zone=clients&lt;/span&gt; &lt;span class="s"&gt;dry_run=on&lt;/span&gt; &lt;span class="s"&gt;log_level=warn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch the log for the bans it reports, confirm no legitimate client is caught, then remove &lt;code&gt;dry_run=on&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fleet-Wide Bans with Redis or Valkey
&lt;/h2&gt;

&lt;p&gt;Behind a load balancer, a per-server ban is theatre: the attacker simply lands on a different node. Abuse Guard closes that gap without ever putting Redis in the request path. Point every node at one Redis or Valkey instance and set &lt;code&gt;redis=on&lt;/code&gt; on the zone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;http&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;abuse_guard_redis&lt;/span&gt; &lt;span class="s"&gt;host=10.0.0.5&lt;/span&gt; &lt;span class="s"&gt;password=secret&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;abuse_guard_zone&lt;/span&gt;  &lt;span class="s"&gt;zone=clients:10m&lt;/span&gt; &lt;span class="s"&gt;redis=on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kn"&gt;abuse_guard&lt;/span&gt; &lt;span class="s"&gt;zone=clients&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;SELinux:&lt;/strong&gt; on enforcing systems (RHEL, Rocky Linux, AlmaLinux) the kernel blocks NGINX from opening the connection to Redis until you permit it once with &lt;code&gt;setsebool -P httpd_can_network_connect 1&lt;/code&gt;. Skip this and replication silently does nothing while local enforcement carries on as normal.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Each node still decides locally and counts locally. The instant a node issues a ban, it broadcasts that one fact to the cluster and records a durable copy; every other node imports it within milliseconds, and a 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. 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 write access as privileged: anything that can write to it can issue bans.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bans That Survive a Restart
&lt;/h2&gt;

&lt;p&gt;Point a zone at a file and active bans are snapshotted on an interval and restored at startup, so a reload or a reboot does not hand every attacker a fresh slate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;abuse_guard_zone&lt;/span&gt; &lt;span class="s"&gt;zone=clients:10m&lt;/span&gt;
                 &lt;span class="s"&gt;persist=/var/lib/nginx/abuse_guard/clients.state&lt;/span&gt;
                 &lt;span class="s"&gt;persist_secret=00112233445566778899aabbccddeeff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The snapshot is integrity-checked and written so that a crash can never leave a torn file. With &lt;code&gt;persist_secret&lt;/code&gt; set, it is also signed with HMAC-SHA256, so a tampered file is rejected rather than trusted. Keep the directory readable only by the NGINX worker user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Abuse Guard vs limit_req and fail2ban
&lt;/h2&gt;

&lt;p&gt;NGINX already ships rate limiting, and most servers already run &lt;code&gt;fail2ban&lt;/code&gt;, so it is worth being precise about where Abuse Guard fits rather than presenting it as a replacement for either.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;limit_req&lt;/th&gt;
&lt;th&gt;fail2ban&lt;/th&gt;
&lt;th&gt;Abuse Guard&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Signal&lt;/td&gt;
&lt;td&gt;Request rate&lt;/td&gt;
&lt;td&gt;Log patterns&lt;/td&gt;
&lt;td&gt;Response error rate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Action&lt;/td&gt;
&lt;td&gt;Throttle / delay&lt;/td&gt;
&lt;td&gt;Firewall ban&lt;/td&gt;
&lt;td&gt;Timed in-worker ban&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where it runs&lt;/td&gt;
&lt;td&gt;In NGINX&lt;/td&gt;
&lt;td&gt;Out of process&lt;/td&gt;
&lt;td&gt;In NGINX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reaction time&lt;/td&gt;
&lt;td&gt;Immediate&lt;/td&gt;
&lt;td&gt;Log-tail delay&lt;/td&gt;
&lt;td&gt;Immediate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best at&lt;/td&gt;
&lt;td&gt;Shaping load&lt;/td&gt;
&lt;td&gt;Host-wide bans across services&lt;/td&gt;
&lt;td&gt;Evicting error-heavy clients&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use a rate limiter to shape load and protect capacity. Use &lt;code&gt;fail2ban&lt;/code&gt; for host-wide bans that span SSH, mail, and other services. Use Abuse Guard for the specific job neither does well: reading the errors NGINX is already returning and evicting the clients defined by them, in the same worker, on the request itself. The three layer cleanly. A common production stack runs &lt;code&gt;limit_req&lt;/code&gt; for burst control and Abuse Guard for eviction in the same &lt;code&gt;location&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Considerations
&lt;/h2&gt;

&lt;p&gt;Abuse Guard is engineered to make abuse cheap to reject and good traffic free to serve. Three properties matter in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rejection is the cheapest outcome.&lt;/strong&gt; A banned client is turned away at the preaccess phase, before routing, file lookups, or upstream connections, so the more an attacker hammers you the less each request costs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory is fixed per client.&lt;/strong&gt; Each tracked identity is one small, constant-size record regardless of threshold, so a single 10 MB zone tracks roughly a hundred thousand clients. That is what lets one zone absorb a botnet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional dependencies fail open.&lt;/strong&gt; Clustering and signed snapshots are best-effort by design. If Redis or the disk misbehaves, enforcement quietly carries on from local memory and your traffic is never held hostage to a dependency.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Cause and fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;nginx -t&lt;/code&gt; reports "unknown directive abuse_guard_zone"&lt;/td&gt;
&lt;td&gt;The module is not loaded. Confirm &lt;code&gt;load_module modules/ngx_http_abuse_guard_module.so;&lt;/code&gt; is in the main context on RHEL-based systems, and that the package is installed: &lt;code&gt;ls /usr/lib64/nginx/modules/ngx_http_abuse_guard_module.so&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bans never trip&lt;/td&gt;
&lt;td&gt;The score decays at &lt;code&gt;threshold ÷ interval&lt;/code&gt; per second. If errors arrive slower than that, the score never builds. Lower &lt;code&gt;threshold&lt;/code&gt; or shorten &lt;code&gt;interval&lt;/code&gt; for the burst you want to catch.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A client is never counted&lt;/td&gt;
&lt;td&gt;Its &lt;code&gt;key&lt;/code&gt; may resolve to an empty string (skipped by design), or it may match an &lt;code&gt;abuse_guard_allow&lt;/code&gt; range. Check both.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong client is banned behind a CDN&lt;/td&gt;
&lt;td&gt;You are keying on the proxy address. Resolve the real client with &lt;code&gt;realip&lt;/code&gt; first, then key on &lt;code&gt;$binary_remote_addr&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;dry_run&lt;/code&gt; bans nothing&lt;/td&gt;
&lt;td&gt;That is correct: &lt;code&gt;dry_run=on&lt;/code&gt; logs the verdict but never writes a ban. Check the log for the bans it reports, then remove &lt;code&gt;dry_run=on&lt;/code&gt; to enforce.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redis replication does nothing on RHEL or Rocky Linux&lt;/td&gt;
&lt;td&gt;SELinux is blocking the outbound connection. Allow it once with &lt;code&gt;setsebool -P httpd_can_network_connect 1&lt;/code&gt;, then reload NGINX. Local enforcement is unaffected either way.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A &lt;code&gt;return&lt;/code&gt; in the same location appears to bypass the guard&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;return&lt;/code&gt; directive can short-circuit request processing. Place enforcement where it runs before any short-circuit, or in a parent scope.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When you run behind a CDN or reverse proxy, never trust a raw &lt;code&gt;X-Forwarded-For&lt;/code&gt;. Let &lt;code&gt;realip&lt;/code&gt; resolve the true client first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;set_real_ip_from&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="s"&gt;.0.0/8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;real_ip_header&lt;/span&gt;   &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;real_ip_recursive&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A tuned Abuse Guard policy only stays correct until someone touches the config.&lt;/strong&gt; NGINX upgrades, module updates, and routine edits quietly reintroduce the very gaps you just closed, and a ban rule that silently stops matching is worse than none. &lt;a href="https://amplify.getpagespeed.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;GetPageSpeed Amplify&lt;/strong&gt;&lt;/a&gt; runs scheduled gixy scans across every host and ties findings to live NGINX runtime metrics. Drop-in compatible with the deprecated &lt;code&gt;nginx-amplify-agent&lt;/code&gt; (EOL January 2026).&lt;/p&gt;

&lt;p&gt;The NGINX abuse guard module turns the errors your server already returns into an automatic, self-calibrating defense. It reads the one signal abusers cannot hide, decides in microseconds inside the worker, and evicts offenders with a real timed ban instead of a throttle that lets them straight back in. With &lt;code&gt;dry_run&lt;/code&gt; for safe rollout, a &lt;code&gt;map&lt;/code&gt;-driven key for surgical scoping, optional fleet-wide replication, and bans that survive a restart, it covers the timed-ban niche that neither &lt;code&gt;limit_req&lt;/code&gt; nor &lt;code&gt;fail2ban&lt;/code&gt; fills. Pair it with the &lt;a href="https://www.getpagespeed.com/server-setup/nginx/nginx-delay-module" rel="noopener noreferrer"&gt;NGINX delay module&lt;/a&gt; to tarpit slow probers, the &lt;a href="https://www.getpagespeed.com/server-setup/nginx/nginx-waf-module" rel="noopener noreferrer"&gt;NGINX WAF module&lt;/a&gt; for payload inspection, or &lt;a href="https://www.getpagespeed.com/server-setup/nginx/install-modsecurity-nginx-linux" rel="noopener noreferrer"&gt;ModSecurity for NGINX&lt;/a&gt; for full rule-based filtering.&lt;/p&gt;

&lt;p&gt;Abuse Guard is available as a pre-built package from the &lt;a href="https://nginx-extras.getpagespeed.com/modules/abuse-guard/" rel="noopener noreferrer"&gt;GetPageSpeed RPM repository&lt;/a&gt; and the &lt;a href="https://apt-nginx-extras.getpagespeed.com/modules/abuse-guard/" rel="noopener noreferrer"&gt;GetPageSpeed APT repository&lt;/a&gt;. For licensing, volume deployments, or a hand getting set up, &lt;a href="https://www.getpagespeed.com/contact-us" rel="noopener noreferrer"&gt;contact us&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nginx</category>
      <category>security</category>
      <category>devops</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
