Abuse Guard configuration reference
Almost every tuning knob the Abuse Guard reads lives in scan_nginx.sh as a
built-in default; then /root/.barracuda.cnf is sourced and replaces any
value it sets (the override is a plain assignment, not a merge). The one
exception is the crawler-fleet family, whose defaults live in its own job,
nginx_fleet.sh, and are read the same way.
This page is the authoritative reference for those variables: the real default, what it controls, and how it interacts with the rest of the scoring engine.
Where defaults come from. Two layers set these values.
scan_nginx.shhard-codes a built-in default for each variable so the script is correct on a box that has never been touched.Separately,
autoupboaseeds a subset into/root/.barracuda.cnf(the_DEFAULT_NGINX_DOS_*block) so they appear as editable lines:_NGINX_DOS_LINES,_NGINX_DOS_LIMIT,_NGINX_DOS_MODE,_NGINX_DOS_DIV_INC_NR,_NGINX_DOS_INC_MIN,_NGINX_DOS_LOG,_NGINX_DOS_IGNORE,_NGINX_DOS_STOP.autoupboaalso normalises_NGINX_DOS_LIMITback to399on each pass.Those eight classics are the entire seeded set: the DDoS, path-flood, ignore-paths, 444-weight and php-probe knobs are not seeded, and neither is any knob of the newer detectors (
_NGINX_HTTP10_AUTH_*,_NGINX_UA_BURST_*,_NGINX_GUARD404_*,_NGINX_I18N_FLOOD_*,_NGINX_FPM_*,_NGINX_FLEET_*) — they all take their script's built-in defaults unless you add them to/root/.barracuda.cnfyourself.Where the seeded value differs from the script default, the seeded one wins — except
_NGINX_DOS_LOG, which is seed-only: an existing uncommented line is never overwritten by a retune, because it is the landing key of the converted.debug.monitor.log.cnfmarker and a set value must survive fleet retunes. The other_NGINX_DOS_*keys are reset to their defaults by a retune (with_NGINX_DOS_LIMITre-normalised to399).
Override semantics
built-in default assigned in scan_nginx.sh
↓
source /root/.barracuda.cnf
↓
any _NGINX_* line there REPLACES the default outright
- A scalar override (
_NGINX_DOS_LIMIT=199) simply wins. - A list override (
_NGINX_DOS_IGNORE_PATHS,_NGINX_PATH_FLOOD_WATCH) replaces the whole list — it is not appended to. Include everything the box needs; an empty value disables that feature. - A few weights (
_NGINX_DOS_444_WEIGHT,_NGINX_PHP_PROBE_WEIGHT) are not in the static default block. They are computed after the config is sourced —_NGINX_DOS_LIMIT / 3— but only if the operator did not set a numeric value. This deliberately preserves an explicit=0(disable) or custom value from/root/.barracuda.cnfand avoids a stale default when the config later lowers_NGINX_DOS_LIMIT.
Window and per-IP scoring
| Variable | Default | What it controls |
|---|---|---|
_NGINX_DOS_LINES |
1999 |
Lines of access.log read on a baseline pass only: the first run, or when the saved offset is lost or the log has rotated. Every other pass reads just the bytes appended since the previous one (about 5 s), so raising this does not widen the detection window. It also sets how many recent lines an i18n-flood snapshot covers. A distinct variable from _NGINX_DOS_LIMIT — not an alternate limit. |
_NGINX_DOS_LIMIT |
399 |
Per-IP score at which an IP is written to web.log. All weights below are derived from it. autoupboa also normalises this to 399 on each pass. |
_NGINX_DOS_MODE |
2 |
Per-IP algorithm. Mode 1 adds extra +5 increments for POST to /user, /user/register|pass|login, /node/add and GET to /node/add and /search; mode 2 (default) skips those. Both modes apply the _NGINX_DOS_STOP check below. |
_NGINX_DOS_LOG |
VERBOSE (script) / SILENT (seeded) |
Log verbosity: SILENT, NORMAL, or VERBOSE. The script's built-in default is VERBOSE, but autoupboa seeds _NGINX_DOS_LOG=SILENT into /root/.barracuda.cnf once (its own internal default is held in _DEFAULT_NGINX_DOS_LOG), so a normally-managed box runs SILENT unless changed. Seed-only: a set value (including the NORMAL the .debug.monitor.log.cnf fold writes) survives retunes, unlike the other _NGINX_DOS_* keys. |
_NGINX_DOS_DIV_INC_NR |
40 |
Divisor for the standard 4xx/5xx increment: _INC_NR = _NGINX_DOS_LIMIT / 40 (≈ 10 at the defaults). |
_NGINX_DOS_INC_MIN |
3 |
Floor for the computed increments — _INC_NR and _INC_S_NR are never less than this. |
The /user/login brute-force increment uses _INC_S_NR = _NGINX_DOS_LIMIT / 80
(≈ 5, floored at _NGINX_DOS_INC_MIN). The divisor _NGINX_DOS_DIV_INC_S_NR is
derived as _NGINX_DOS_DIV_INC_NR × 2 and is not a separately tunable knob.
| Variable | Default | What it controls |
|---|---|---|
_NGINX_DOS_444_WEIGHT |
_NGINX_DOS_LIMIT / 3 (≈ 133), computed |
Extra weight per confirmed 444 on a watched attack path (on top of _INC_NR). Honoured as set if numeric in config; 0 disables. |
_NGINX_PHP_PROBE_WEIGHT |
_NGINX_DOS_LIMIT / 3 (≈ 133), computed |
Extra weight for a .php request path that 404s on a Drupal docroot (webshell probe). Honoured as set if numeric in config; 0 disables. |
_NGINX_MIN_BLOCK_REQS |
3 |
Minimum raw (unweighted) requests an IP must make in the window before it can be individually blocked — stops one heavily-weighted hit from banning a one-request IP. Set to 1 to disable the floor. |
_NGINX_DOS_STOP — SQLi / blind-timing injection regex
_NGINX_DOS_STOP="WAITFOR.DELAY|DECLARE.*@x|/\*\*/|%27.*%29.*%3B|0x[0-9a-f]{6}"
This is not a generic flood-rate knob and not a "+5 counter" increment. It is a regular-expression set of SQL-injection / blind-timing fingerprints:
WAITFOR DELAY- a
DECLARE @xpayload - inline
/**/comment obfuscation - the
%27…%29…%3BURL-encoded') ;close - a long hex literal
When a log line
matches, _process_ip adds the full _NGINX_DOS_LIMIT to that IP's counter in
a single hit, saturating its score to the block threshold at once. A block still
also requires the IP to clear the _NGINX_MIN_BLOCK_REQS raw-request floor
(default 3), so a lone probe from an otherwise idle IP is caught in aggregate,
not instantly.
The regex is matched (unanchored) against the whole log line, so an injection attempt anywhere in the request trips it.
A
+5increment is sometimes mis-attributed to_NGINX_DOS_STOP. That is wrong on both counts: the+5increments belong to the mode-1/user,/node/addand/searchpatterns, while_NGINX_DOS_STOPadds the full_NGINX_DOS_LIMIT, not+5.
The default is also seeded into /root/.barracuda.cnf by autoupboa
(_DEFAULT_NGINX_DOS_STOP). An empty value disables injection-keyword scoring
entirely.
DDoS — shared-UA aggregate (detector 2)
See Detector 2 for the mechanism.
| Variable | Default | What it controls |
|---|---|---|
_NGINX_DDOS_UA_IP_THRESHOLD |
100 |
Distinct IPs sharing one User-Agent in the scan window before that UA is treated as an attack fingerprint. |
_NGINX_DDOS_UA_REQ_THRESHOLD |
1000 |
Total per-UA requests (across all IPs) that flags a UA even when its IP count is low but request volume is extreme — ~1000 in the ~5 s window is ~200 req/s of one exact UA string. |
_NGINX_DDOS_IP_MIN_REQS |
20 |
When a UA is flagged, only block contributing IPs that made at least this many requests with it. A complete human search session (results page, per-keystroke autocomplete, AJAX views, result clicks) stays clear of 20, while one IP hammering a shared UA does not. |
The defaults are sized for high-traffic boxes: within the ~5 s scan window the single most common real mobile-browser UA string is shared by well over 20 distinct IPs, and a genuine distributed botnet randomises its UA per IP — one UA shared by many IPs is the signature of a popular browser, not a bot.
Tune
per box in /root/.barracuda.cnf; go tighter only on evidence of a genuinely
homogeneous burst. Abusive single IPs are still caught by the
per-IP scorer and the path-flood detector below.
Path-flood — search-amplification aggregate (detector 3)
See Detector 3 for the mechanism.
| Variable | Default | What it controls |
|---|---|---|
_NGINX_PATH_FLOOD_IP_THRESHOLD |
30 |
Distinct IPs on a watched path prefix before a flood is declared. Both 200 and 444 responses count towards this. Declaration alone never bans — the per-IP gate (_NGINX_PATH_FLOOD_IP_MIN_REQS) decides who is blocked — but a low value wastes work and widens the blast radius on a legitimate traffic peak. Tune upward further if a flood is still declared on legit peaks. |
_NGINX_PATH_FLOOD_REQ_THRESHOLD |
100 |
Total requests (200 + 444) to the prefix before a flood is declared — ~100 in the ~5 s window is ~20 req/s of search traffic site-wide, above legitimate interactive use, well below a real amplification flood. 444s come free from Nginx but still feed the aggregate so a one-request-per-IP distributed flood is caught. |
_NGINX_PATH_FLOOD_SLOW_SECS |
3 |
Upstream seconds above which a 200 counts as "slow" (real backend cycles consumed) and earns an extra per-IP increment. |
_NGINX_PATH_FLOOD_IP_MIN_REQS |
20 |
Per-(prefix, IP) 200-response count before that IP is listed during a flood. Kept deliberately modest at 20: under the default _NGINX_DOS_MODE=2 a backend 200 scores only +1 in the per-IP scorer, so a moderately heavy search scraper is caught primarily here — yet 20 still clears a shared CGNAT or Apple Private Relay egress aggregating many real users. Set to 1 to list every 200-sending participant. |
_NGINX_PATH_FLOOD_WATCH |
see below | Pipe-separated patterns matched against the full log line (path and query string) that mark a prefix as expensive/watched. |
_NGINX_PATH_FLOOD_WATCH="apachesolr_search|search_api_views_fulltext|search_api_fulltext|im_taxonomy_vid|/search/node|/search/user"
The defaults cover legacy Apache Solr search paths, the Search API Views and programmatic fulltext query parameters, the faceted-search taxonomy facet parameter, and Drupal core node/user search. Add site-specific expensive endpoint substrings per box; the override replaces the list.
Worked example — an expensive path of your own
The classic case is a calendar: a /events/<year>/<month>/<day> (or
?date=) space is a cheap page for a human and a bottomless one for a
crawler that wants "every single date of every single year", each request an
uncached render. Nothing here is a single knob — the layers below already act
before you change anything, and the watch list is the one you add to:
- A crawler that says who it is is sorted by class at the edge. AI
classes, scrapers and the
$is_crawlerset are answered444(or rate-limited per vendor) by the edge maps. A declared search crawler is served like a visitor, and$deny_on_high_loadanswers it503only once Spider Protection arms at high load, so a fleet that borrows a search engine's name renders pages until then. - A fleet that lies about who it is is caught in aggregate:
Detector 5
on the shared forged browser name, Detector 2
on volume — but only when its answers are mostly bad statuses. A
date-space crawl answers
200every time, so those two stay quiet, on purpose. - The site stays up regardless because of the
per-vhost cap on anonymous renders:
it bounds how many uncached renders one site may hold in flight, so the
front cache keeps absorbing the herd instead of collapsing under it. It
ships at
100, which is inert behind a small pool — tune it per instance toward 1.5× the pool'spm.max_children(FPM capacity) before you expect it to bite. - To have the participants banned, put the path on the watch list. In
/root/.barracuda.cnf, restate the whole list plus your substring (the override replaces, it does not append):
_NGINX_PATH_FLOOD_WATCH="apachesolr_search|search_api_views_fulltext|search_api_fulltext|im_taxonomy_vid|/search/node|/search/user|/events/"
From the next scorer tick that prefix is declared under flood once 30
distinct addresses or 100 backend answers hit it inside the window, and each
address that reached the backend at least 20 times (200 answers, not the
444s the maps already shed) is written to web.log and temp-banned for 15
minutes on ports 80/443 by the ban pipeline; repeat
offenders escalate to a persistent deny. A render slower than 3 s counts
extra. The thresholds are the search-flood defaults and sit above a busy
public page's legitimate peak — lower them only on a real report, and remember
that detection is post-hoc: a burst that finishes inside one 5-second window is
served cheaply and banned for its next visit, never mid-burst.
Two things to keep straight: the substring is matched against the whole log
line, so choose one that cannot occur in ordinary URLs (/events/ catches
/events/2026/09/03 and ?q=events/… alike; a bare events would also match
/news/events-recap); and the search engines you want indexing that calendar
are exempt through the whitelist, so a
legitimate crawl of the same space is neither declared nor banned.
HTTP/1.0 auth-spam (detector 4)
Catches the slow registration/credential-spam botnet that POSTs to Drupal auth paths over HTTP/1.0 with a forged modern-browser User-Agent — no browser built in the last ~15 years speaks HTTP/1.0 to a public HTTPS host, so the protocol itself is the tell.
The bot paces roughly one request per IP every few minutes from a small CIDR block, staying under every volume detector above; this detector instead accumulates hits in a sliding window that persists across runs and bans through the standard pipeline. See the scoring engine for the mechanism.
| Variable | Default | What it controls |
|---|---|---|
_NGINX_HTTP10_AUTH_DETECT |
YES |
Master switch. NO opts the box out — required behind a non-BOA front proxy or CDN that downgrades to HTTP/1.0 at origin. BOA's own proxy layer sets proxy_http_version 1.1, so a correctly-updated BOA proxy tier does not downgrade; confirm via the access log that real clients show HTTP/1.1 / HTTP/2 before opting out. |
_NGINX_HTTP10_AUTH_PATHS |
see below | Bash ERE matched against the parsed, query-stripped request URI; the optional two-letter prefix mirrors i18n paths. Must be a valid ERE if overridden — a malformed override is detected at startup by an rc>1 [[ =~ ]] probe and reverted to the default with a warning, so a typo cannot silently disable the detector. |
_NGINX_HTTP10_AUTH_WINDOW |
600 |
Sliding-window length in seconds, persisted across runs in /var/xdrago/monitor/log/http10_auth.window. |
_NGINX_HTTP10_AUTH_IP_THRESHOLD |
3 |
Windowed per-IP auth-path hits at which the IP is banned. A lone stray HTTP/1.0 hit stays below it. |
_NGINX_HTTP10_AUTH_CIDR_THRESHOLD |
6 |
Windowed /24-aggregate hits at which every observed contributing IP of the /24 is banned — never an unseen address in the /24. Set very high to rely on the per-IP path only. |
_NGINX_HTTP10_AUTH_PATHS="^/([a-z]{2}/)?user/(register|password)(/|$)"
Non-positive-integer overrides of the three numeric knobs revert to the
defaults. None of these is autoupboa-seeded — they take the script defaults
unless added to /root/.barracuda.cnf by hand.
UA-burst scanner fleet (detector 5)
The per-IP scorer and detector 2 both exclude 301 redirects and both need
high per-IP or per-UA volume, so a distributed scanner fleet — dozens of cloud
IPs sharing one forged UA, a few requests each, mostly redirects and 404s to
non-Drupal CMS paths — slips through every gate above.
This detector groups by
UA, counts all statuses including 301, and trips only when a UA is shared
by many IPs and its traffic is overwhelmingly "bad".
| Variable | Default | What it controls |
|---|---|---|
_NGINX_UA_BURST_DETECT |
YES |
Master switch; NO opts the box out. |
_NGINX_UA_BURST_IP_MIN |
12 |
Minimum distinct IPs sharing one exact UA in the scan window. |
_NGINX_UA_BURST_REQ_MIN |
60 |
Minimum total requests for that UA across all its IPs in the window. |
_NGINX_UA_BURST_BAD_PCT |
80 |
Minimum percent of the UA's requests that are "bad" — 3xx redirects (301/302/307/308) or client errors (400/403/404/410) — before the fleet is declared hostile. This is the false-positive keystone: a legitimate browser fleet is mostly 200/304 and stays far below it. |
_NGINX_UA_BURST_IP_MIN_BAD |
3 |
Per-IP bad-probe floor once the fleet trips — a legitimate visitor sharing the UA sent 200s, zero bad probes, and is never blocked. |
Non-positive-integer overrides revert to the defaults (the bad-ratio division
requires _NGINX_UA_BURST_REQ_MIN of at least 1). Not autoupboa-seeded. Tune
tighter, never looser, on real reports.
Guard-404 scraped-interactive-path (detector 6)
The no-referer request gates answer cold GETs to interactive-only paths (Flag
toggles, HybridAuth windows) with a static 404 — each one a per-request bot
verdict nginx already computed. This detector surfaces the campaign behind
those gates once enough distinct IPs trip them at once, and can additionally
ban the repeat-offender tail (cutting off the same address's parallel
content-crawl 200s that no other detector counts). See the
scoring engine for the mechanism, the crawler
protections, and the rationale for excluding print paths.
The per-IP ban is opt-in and off by default. Unlike the HTTP/1.0 tell, this
signal is browser-inducible: any third-party page can carry an <img referrerpolicy="no-referrer"> pointed at a hosted site's guarded path and make
an innocent visitor's browser emit the exact trigger, so a default-on ban would
let a stranger's page deny your visitors box-wide. Arm BAN per box on a
confirmed campaign, the same way _NGINX_HARVEST_ACTION is armed.
| Variable | Default | What it controls |
|---|---|---|
_NGINX_GUARD404_DETECT |
YES |
Master switch; NO opts the box out. |
_NGINX_GUARD404_ACTION |
REPORT |
REPORT logs GUARD404-WOULD-BAN lines and bans nothing; BAN arms the per-IP ban. Any other value falls back to REPORT, so a typo cannot silently arm it. |
_NGINX_GUARD404_PATHS |
see below | Bash ERE matched against the parsed, query-stripped request URI — the gate-family path class, case-insensitive like the nginx maps. Must be a valid ERE if overridden; a malformed override is detected at startup and reverted to the default with a warning. |
_NGINX_GUARD404_WINDOW |
600 |
Sliding-window length in seconds, persisted across runs in /var/xdrago/monitor/log/guard404.window. |
_NGINX_GUARD404_IP_THRESHOLD |
6 |
Windowed gate-404 count at which an IP is reported (or banned, in BAN mode). Sized above what a real visitor can produce — the gate's own 404 invites retries. |
_NGINX_GUARD404_CAMPAIGN_IPS |
40 |
Distinct windowed IPs at which one GUARD404-CAMPAIGN alert + forensic snapshot goes to i18n_flood.log. Alert-only, and it runs in both modes. |
_NGINX_GUARD404_COOLDOWN |
3600 |
Campaign alert cool-down in seconds. |
_NGINX_GUARD404_PATHS="^/([A-Za-z]{2}(-[A-Za-z]+)?/)?([Ff][Ll][Aa][Gg]/(flag|unflag|FLAG|UNFLAG)/[A-Za-z0-9_]+/[0-9]|[Hh]ybrid[Aa]uth/window/[A-Za-z0-9_.-]+/?$)"
Non-positive-integer overrides of the numeric knobs revert to the defaults. Not
autoupboa-seeded — script defaults apply unless added to /root/.barracuda.cnf
by hand. There is deliberately no /24 aggregation knob: this botnet class
rides ISP-dispersed CGNAT space where a /24 holds real users.
clearwebbans clears this detector's window (and the HTTP/1.0 one) along with
the ban logs, so an operator unban is not undone by carried-forward state.
Crawler-fleet fingerprint refusal
The one detector on this page that is not part of scan_nginx. The
crawler-fleet refusal lives in its own per-minute job,
/var/xdrago/nginx_fleet.sh, which declares a (vhost, user agent) fingerprint
and renders the Nginx maps that answer its members 429 — see
request guards
for the enforcement and
the ban pipeline
for the generator. Its knobs are read from /root/.barracuda.cnf the same way,
and every one of them is validated: a non-numeric value, or one outside the
range below, is discarded with a CONFIG line in
/var/xdrago/monitor/log/fleet.log and the shipped default is used, so a typo
can never turn a gate into a hair trigger.
The refusal is ON by default, on every box, in BOTH scopes. There is no
REPORTsoak and no per-box arming step:_NGINX_FLEET_ACTIONships asBANand_NGINX_FLEET_CRAWLER_ACTIONinherits it. Action values are case-insensitive, and an invalid address action falls back to the shippedBANwith aCONFIGline rather than switching the protection off — a typo must never disarm it. An invalid crawler action inherits the (already validated) address action.These knobs exist for reactive loosening after a confirmed legitimate report, not for arming. Loosen the one scope that produced the report, leave the other refusing.
| Variable | Default | Range | What it controls |
|---|---|---|---|
_NGINX_FLEET_DETECT |
YES |
YES / NO |
Master switch. NO also releases every live refusal on the next run, because the store is then deleted rather than kept. |
_NGINX_FLEET_ACTION |
BAN |
BAN / REPORT |
Address scope: browser-shaped agents and every agent that does not name itself a crawler. REPORT logs WOULD-BAN and neither stores nor renders that scope's fingerprints or members. |
_NGINX_FLEET_CRAWLER_ACTION |
(empty) | BAN / REPORT / empty |
Network scope: self-declared crawler names, refused per /16. Empty inherits _NGINX_FLEET_ACTION. |
_NGINX_FLEET_WINDOW |
300 |
180–900 | Analysis window in seconds: how much of the recent log one pass considers. |
_NGINX_FLEET_MIN_SPAN |
180 |
60–900 | Minimum age of the oldest line in the read tail before a pass may declare anything. A tail that spans less is skipped; the NOTE telling you to raise _NGINX_FLEET_TAIL_MB is written only when the tail was truncated, so a short log after a rotation is skipped silently. A value above _NGINX_FLEET_WINDOW reverts to 180. |
_NGINX_FLEET_TAIL_MB |
64 |
8–512 | Megabytes read from the end of access.log each pass. |
_NGINX_FLEET_IP_MIN |
32 |
12–100000 | Distinct non-allow-listed addresses a route class must be pulled from before it can be declared. |
_NGINX_FLEET_REQ_MIN |
48 |
24–1000000 | Requests that route class must hold in the window. |
_NGINX_FLEET_SHARE_PCT |
8 |
2–100 | Minimum percent of the vhost's windowed traffic the class must represent — a fleet that is noise against the site's real volume is left alone. |
_NGINX_FLEET_NOREF_PCT |
95 |
50–100 | Minimum percent of the class that must carry no Referer. Also the per-address bar for joining the address scope. |
_NGINX_FLEET_UNIQ_PCT |
80 |
50–100 | Minimum percent of requests hitting a distinct URL — a crawl, not a re-fetch. |
_NGINX_FLEET_BAD_PCT |
5 |
0–100 | Maximum percent of the class that may be 5xx or 444. A cohort that is mostly already being refused is a different problem and is not declared. |
_NGINX_FLEET_ALLOW_PCT |
20 |
1–100 | If allow-listed addresses reach this share of the class's distinct addresses, the pass logs REALIP-SUSPECT and declares nothing — the shape of a box whose realip trust chain is wrong. |
_NGINX_FLEET_CAND_IPS |
16 |
4–100000 | Addresses a live fingerprint must still show in a later window before that pass records more members. |
_NGINX_FLEET_NET_MIN_IPS |
2 |
2–1000 | Member addresses required inside one /16 before that network is refused. |
_NGINX_FLEET_TTL |
3600 |
600–86400 | Seconds a declared fingerprint lives. Its expiry releases all of its members at once. |
_NGINX_FLEET_MEMBER_TTL |
21600 |
600–604800 | Seconds a member address or network lives, while its fingerprint is live. This bounds the steering collateral. |
_NGINX_FLEET_RELOAD_GAP |
600 |
0–3600 | Minimum seconds between reloads for a member-only change. A new or expired fingerprint reloads at once. |
_NGINX_FLEET_MAX_FP |
16 |
1–64 | Live fingerprints kept per box; the newest-expiring win and the rest are dropped with a CAP line. |
_NGINX_FLEET_MAX_ENTRIES |
20000 |
100–100000 | Keys per rendered map (addresses, networks); the newest win, with a CAP line. |
_NGINX_FLEET_UA_EXEMPT |
(empty) | ERE | Additive. Whatever is set here is appended to the shipped exemption roster; it can never remove a shipped exemption. An override that makes the combined pattern invalid is dropped with a CONFIG line and the shipped roster alone applies. |
The shipped roster exempts the search engines and their special-case fetchers,
the social and messaging link previewers, the uptime monitors, and the AI classes
that are allowed by default — the ones the
AI crawler policy governs with
its own per-vendor aggregate limits. Those agents are never declared as a fleet,
whatever their volume; a declared class that matches the roster is logged
EXEMPT and skipped.
Two more bounds are internal, not knobs: a user agent that cannot be carried
as an exact map key (the printable-ASCII grammar, or longer than the key limit
the master render's map_hash_bucket_size allows) is reported UNBANNABLE and
never stored, so it cannot occupy a _NGINX_FLEET_MAX_FP slot a refusable fleet
needs; and one pass records at most 2000 addresses per fingerprint.
Not autoupboa-seeded — the script's defaults apply unless the variable is added
to /root/.barracuda.cnf by hand.
Tier B — i18n flood + FPM saturation (alert-only)
These two paths alert and snapshot only — no per-IP bans. A translation flood spreads across thousands of IPs at one or two requests each, so per-IP banning is futile; the value is the early alert plus a forensic snapshot of top talkers, UAs and path classes.
Alerts land in
/var/xdrago/monitor/log/i18n_flood.log; snapshots, the cross-run
window.state and the fpm_maxchildren.pos byte-offset file live under
/var/xdrago/monitor/log/i18n_flood/.
| Variable | Default | What it controls |
|---|---|---|
_NGINX_I18N_FLOOD_DETECT |
YES |
Master switch for the localised-request flood detector. |
_NGINX_I18N_FLOOD_WINDOW |
120 |
Sliding-window length in seconds, persisted across runs. |
_NGINX_I18N_FLOOD_MIN_REQS |
400 |
Windowed localised requests to one vhost before the volume+stress path can trip. |
_NGINX_I18N_FLOOD_STRESS_PCT |
15 |
Backend-stress gate: percent of windowed localised requests that are slow, 5xx or 444 before the volume+stress path trips. |
_NGINX_I18N_FLOOD_SLOW_SECS |
3 |
Slow threshold (whole seconds) for the stress gate. |
_NGINX_I18N_FLOOD_C444_THRESHOLD |
40 |
Early trip: windowed localised 444s to one vhost that trip on their own, independent of volume. Attribution caveat: limit_conn_status is one-per-context, so these 444s come from either the Tier-A i18n guardrail or the general boa_perhost_anon render cap on the same vhost (see request guards) — a general flood can trip this early path too. |
_NGINX_I18N_FLOOD_COOLDOWN |
300 |
Per-vhost alert cool-down seconds, so a multi-minute burst yields a handful of records, not one per scan cycle. |
_NGINX_FPM_SAT_DETECT |
YES |
Master switch for the PHP-FPM saturation trigger. |
_NGINX_FPM_ERR_GLOB |
/var/log/php/php*-fpm-error.log |
Glob of PHP-FPM per-version error logs the trigger reads — byte-offset tracked per file, so only new ceiling hits since the last run are acted on. |
_NGINX_FPM_SAT_PATTERN |
reached max_children setting |
The literal the FPM master logs when a pool hits its pm.max_children ceiling. |
Exemptions
These two knobs exempt at different scopes:
_NGINX_DOS_IGNORE_PATHSis tested at loop scope before any detector runs (_is_ignored_request … && continue), so it exempts the line from all six detectors and the Tier-B i18n-flood tally at once. The two passes that never ride the loop stay out of its reach: the Tier-B FPM-saturation trigger tails the PHP-FPM error logs, and the post-loop harvest pass reads its own bounded tail and exempts by UA (_NGINX_HARVEST_UA_EXEMPT), not by path._NGINX_DOS_IGNOREis tested inside_process_ipand only suppresses the per-IP counter for that line — every other detector still sees it.
The path-exemption mechanism (the laundering-proof URI parse) is documented in detail in The exemption gate; these are the configuration rows.
| Variable | Default | What it controls |
|---|---|---|
_NGINX_DOS_IGNORE |
doccomment |
Keyword(s) that, when found on a 200/302 line, exempt it from per-IP scoring (a regex fragment, matched against the line). |
_NGINX_DOS_IGNORE_PATHS |
see below | Space-separated URI prefixes exempt from all IDS scoring. |
_NGINX_DOS_IGNORE_PATHS="/shopify/webhook /quickbooks/webhook /stripe/webhook /paypal/webhook /github/webhook /gitlab/webhook /graphql /public-api /oauth2"
_NGINX_DOS_IGNORE_PATHS rules:
- Paths are space-separated, each a leading-slash absolute path with no
trailing slash (
/shopify/webhook, not/shopify/webhook/). - Each entry matches that exact path or any sub-path under it (
/graphqlexempts/graphql/api/endpoint). - The match is against the real
$requestURI only — query stripped, any..or%-escape refused — so a token in a User-Agent, Referer, or query string can never launder an exemption (see page 01 for why). - The override in
/root/.barracuda.cnfreplaces the default list, so include every endpoint the box needs. An empty value disables the feature.
These exist because machine/API endpoints authenticate per request at the
application layer (HMAC, OAuth), not by IP — counting them per-IP self-bans a
legitimate webhook provider or API client. The defaults are shipped in the script
(not only the per-box override) so the exemption survives a /root/.barracuda.cnf
regeneration.
Where these are set
All of the above are read by scan_nginx.sh — bar the _NGINX_FLEET_* family,
read by nginx_fleet.sh — and both source /root/.barracuda.cnf, so any of them
can be overridden there.
The consolidated _NGINX_* knob list with
types and source references is in the
Variables reference; the control-file format and
the broader DoS-protection family sit in the
control files & INI topic.
Once an IP is scored over
_NGINX_DOS_LIMIT, enforcement proceeds through
the ban pipeline and the
request guards.
Related
- scan_nginx scoring engine — the detector logic these knobs tune.
- The ban pipeline — the
.instant.csf.block/.full.csf.cleanupmarkers and the escalation thresholds. - Operations + tuning — applying these overrides in practice and enabling debug output.
- Variables reference — the consolidated
_NGINX_*table.