> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dnzwithazee.com/llms.txt
> Use this file to discover all available pages before exploring further.

# The WAF page

> See what the web application firewall caught and where it came from on a world map, look an attacker's address up, block it, ban it for a while, challenge lists of bad networks, decide what AI crawlers may do, catch fake Googlebots, and fix a false alarm in one click.

**Proxy → WAF** brings the [web application firewall](https://docs.dnzwithazee.com/proxy/waf.md) into one place: what it caught,
where it came from, and what to do about it. It shows once a [DNZ Proxy](https://docs.dnzwithazee.com/proxy/dnz-proxy.md) is
approved. Traefik and Caddy have no WAF.

Each node counts what its WAF matched and keeps it. DNZ asks the nodes when you open the page, and
adds a place and a network to each address from its own databases. No address is sent to an
outside service.

## What it shows

Pick a span (1 hour to 30 days), a proxy, private or public proxies and a hostname at the top.

| Part | What you get |
| - | - |
| **Totals** | Blocked requests (answered with the `403` *Access blocked* page), logged ones (log-only routes let them through), their share of all traffic, the addresses and countries they came from, the most common attack type |
| **Where it came from** | A world map: the deeper the colour, the more requests. **Hover** a country for its count, share and rank. **Click** it for its addresses, the rules they set off and the hostnames they went for. The list beside the map lights the map up as you move over it |
| **Over time** | Blocked and logged requests per minute or hour |
| **Attack types, rules, addresses, hostnames, paths** | The busiest of each. Click a rule for its events, an address for its details, a hostname to show only that one |
| **Events** | Each request the WAF matched, newest first. Filter by action, rule, country, address, or search a path, user agent or request ID. Open a row for what the rule catches, the full URL, the answer and the node |
| **Coverage** | How many DNZ Proxy routes have the WAF, blocking or log only, at which sensitivity. Pick the routes without it and turn it on |
| **Shields** | [Virtual patches](https://docs.dnzwithazee.com/proxy/shields.md) for apps that can't be upgraded: each one's CVE, product and release, the routes with it on, what it stopped, and routes worth a look |
| **Campaigns** | The addresses grouped by network, across hostnames, with what they did (see below) |
| **Banned now** | Addresses refused for a while, with the time left, why and where, and **Unban** (see below) |
| **Blocked from this page** | Everything you blocked here, with **Unblock** |
| **Location data** | Where places and networks come from (see below) |
| **Lists of bad networks** | Outside lists DNZ keeps up to date, their size and last update (see below) |
| **Bots** | Humans, verified bots, AI crawlers and fake bots per hostname, the top crawlers, the fake ones' addresses (see below) |
| **Leaked passwords** | The [leaked password](https://docs.dnzwithazee.com/proxy/leaked-passwords.md) filter, the routes checking logins and the nodes serving them |
| **Previews of a higher sensitivity** | What a route would also block at High, before you raise it, and the weekly report (see below) |

> [!NOTE]
>
> Events come from the nodes' request logs, which keep a sample of what they serve. Blocked requests
> get ten times the room, so the totals are exact while the event list can miss some. Nodes older
> than DNZ 2.5 can't filter their log for the WAF: the page names them until they are updated.

## Private or public proxies

**All / Private / Public** beside the proxy filter looks at the attacks on local sites and those on public sites apart:
which addresses, from where, against what. The category is the proxy's own
[access](https://docs.dnzwithazee.com/proxy/overview.md#private-and-public-proxies), the one Serve on's *All private* and *All public* use. It can be set together with one proxy
(both must hold), it stays in the page's address (`?access=private`) so a view can be shared, and the line under the
filters says which proxies are being shown.

A filter that leaves no proxy (private with a public proxy picked, or no proxy marked private) shows nothing and says
why. It never shows everything.

| Part of the page | With the filter |
| - | - |
| Map, totals, chart, top lists, events, campaigns, bots, an address's and a country's panels | Only the nodes of those proxies are asked |
| Shields, coverage, leaked passwords' routes and nodes, each proxy's ban settings | Those proxies only |
| Banned now | Bans on those proxies, and bans on every DNZ Proxy, which reach them too |
| Blocked from this page, an address's firewalls | The firewalls running on those proxies. A firewall can serve several proxies; the list says how many blocks are only on other proxies |
| Decoy names | Visits to those proxies. A DNS query belongs to no proxy: those are left out, and the section says how many |
| Lists of bad networks, the leaked password filter, location databases | Every proxy's, and the section says so |

> [!NOTE]
>
> **A block or a ban keeps its own scope.** It goes where you pick in its dialog (these hostnames, every route with the
> WAF, every DNZ Proxy), whatever the page is showing, and the dialog says so. Unblock takes the value out of every
> firewall, those of other proxies too.

Nothing is added to the request path and nothing is stored: the filter only picks which nodes DNZ asks.

## Look an address up

Click any address on the page.

* **Place**: the country, and the region and city with a city database.
* **Network**: its ASN and owner, such as `AS14061 DigitalOcean, LLC`. **Hosting** marks the network
  of a company that rents out servers: requests from there are usually scripts, not people.
* **Reverse name**: the name the address points back to. **Confirmed** means that name points to the
  address again; an unconfirmed name proves nothing, since anyone can write any name in their own
  reverse zone.
* First and last seen, the rules it set off, the hostnames, paths and user agents it used, and its
  latest events.
* The firewalls already blocking it.

## Block an attacker

**1. Pick what to block**

In an address's details: **Block address**, **Block** its network (the `/24` of an IPv4
address, the `/48` of an IPv6 one) or **Block** its country. A country's panel and each event
offer the same.

**2. Pick where**

**These hostnames** (the default) blocks it on the routes serving the hostnames it went for, so
nothing else is touched. **Every route with the WAF** blocks it on every DNZ Proxy route whose
WAF is on.

**3. Block**

Its requests get the `403` page before they reach your servers, within seconds on every node.

A block is a rule in each route's [firewall](https://docs.dnzwithazee.com/proxy/dnz-proxy.md#firewall-dnzfirewall):
`waf-page-addresses` holds the addresses and ranges, `waf-page-countries` the countries. Both go
after the firewall's own rules, so your allow rules still act first. A route without a firewall gets
the shared `waf-blocklist` firewall. A firewall other routes use blocks the address there too.
**Unblock** takes it out of every firewall again.

## Bans that expire

A block stays until you take it out. A **ban** ends by itself: an address the WAF blocked too often
is refused for a while, then let back in. It works like fail2ban, but across nodes.

Bans are a DNZ Proxy feature on HTTP routes: Traefik and Caddy have no WAF to count, and a TCP or UDP
route has no page to answer with.

They are off by default. Turn them on per proxy, in its DNZ Proxy settings under **Bans that expire**:

| Setting | What it does |
| - | - |
| **Blocks** within **minutes** | How many WAF blocks by one address ban it (20 within 5) |
| **Ban for** | How long the ban lasts (60 minutes) |
| **Who refuses it** | **This proxy**: the nodes of the proxy whose node saw the attack (the default). **Every DNZ Proxy**: all nodes of all DNZ Proxies |
| **Private and loopback addresses too** | Off, an address on a private network is never banned on its own |

The node that sees an address cross the number bans it at once, without waiting for DNZ, and the
other nodes have the ban within seconds. Only blocks count: a route in log-only mode bans nobody.

A banned visit gets the `403` *Access blocked* page with its request ID, never a dropped connection,
so a real visitor banned by mistake sees why and has something to send you. The request log shows
`block ban:<rule>`, with the rule that led to the ban.

> [!NOTE]
>
> A ban never stops the entry point's trusted forwarders, addresses the route's WAF allows, what the
> route's firewall lets through with an allow rule, or DNZ's own uptime checks.

**Banned now** lists them, newest first, with **Unban**. To ban one address by hand for an hour, pick
**Ban for an hour** in the block dialog; this works without the settings above. Ranges and countries
are for blocks. On the map and the chart, banned requests have a colour of their own, and the switch
above the map folds them into blocked. Either way they count as blocked in the totals, in Analytics
and in attack alerts.

Off, a visit reads one setting it already holds and does nothing more; on, it does one lookup, which
measured the same as off with 10,000 bans held ([Proxy benchmarks](https://docs.dnzwithazee.com/proxy/benchmarks.md#what-each-feature-costs)). A node
older than DNZ 2.5 doesn't ban, and the panel names it. A node restarted while DNZ is away holds no
bans until it connects again.

[Decoy names](https://docs.dnzwithazee.com/proxy/decoys.md) ban through these too, whether or not automatic bans are on: a visit to a
name nobody has a reason to open is refused from then on with the note `block ban:decoy`. *Decoy names*
on this page lists who touched what.

## Lists of bad networks

Outside lists of bad networks, kept up to date by DNZ. Your nodes never reach the internet for them:
DNZ downloads each list and sends it to the nodes that need it.

| List | Name | What it is | Terms |
| - | - | - | - |
| [Spamhaus DROP](https://www.spamhaus.org/blocklists/do-not-route-or-peer/) | `spamhaus-drop` | Networks taken over by spammers and criminals, IPv4 and IPv6. EDROP is part of it since 2024. Downloaded once a day, as Spamhaus asks | Free for any use, with credit to The Spamhaus Project |
| [Tor exit nodes](https://metrics.torproject.org/collector.html) | `tor-exit` | Addresses Tor traffic leaves from, from Tor Metrics, every 6 hours | CC0 |
| [abuse.ch Feodo Tracker](https://feodotracker.abuse.ch/blocklist/) | `feodo` | Command-and-control servers of banking botnets, every 6 hours. Often short or empty | CC0 |

Lists are off by default, and a list does nothing by itself. A DNZ Proxy firewall rule names them, so
it applies to the routes you pick and costs one lookup per request only there (nothing measurable with a
list of 100,000 ranges, which takes a node about 8 MiB):

**1. Add a rule to a firewall**

In **Proxy → Middlewares**, open a firewall and add a rule with the condition **Network list**.
DNZ downloads a list as soon as a rule names it.

**2. Pick what it does**

A rule on a list **challenges** by default: the *Under attack* browser check, which real browsers
pass on their own and scripts don't. It can block or only log instead.

**3. Give the firewall to your routes**

In the route dialog, or with batch edit.

In JSON, the condition is `{ "field": "list", "op": "in", "values": ["tor-exit", "spamhaus-drop"] }`.

> [!WARNING]
>
> Saving such a rule is refused while a node of a proxy using that firewall is older than DNZ 2.5,
> naming the node: an older node would turn the routes using it off. What isn't a browser (an app, a
> monitor) can't pass a challenge and gets a `403`, so add an allow rule above it for those you trust.

DNZ leaves out private and reserved ranges and anything wider than a `/8` (`/16` for IPv6), and keeps
the last good copy when a download fails. Lists are read by the node's firewall, so a Traefik or
Caddy proxy has none. A list can't be turned off while a rule names it.

## Campaigns

The addresses the WAF matched, grouped by network (ASN), across hostnames, with what they did:

> AS4134 Chinanet: 3 addresses, 291 requests, 2 hostnames, Path traversal and SQL injection

Without the ASN database (see Location data) they group by `/24` (`/48` for IPv6). Times and hostnames
come from the latest events, so they may leave out some on a busy day. Open an address for its
details, or block the campaign's addresses or their `/24`s: the same block as above. Blocking a whole
network by its ASN isn't offered.

## Bots

Decide what AI crawlers may do on a route, and catch visitors who only claim to be a search engine. Both
are off by default and run on DNZ Proxy.

> [!WARNING]
>
> Traefik and Caddy can't do this. A setting on one of their routes is kept but does nothing: the proxy's
> page warns *AI crawler settings are DNZ Proxy only*, and the crawler gets in.

Only routes that ask look at a request's user agent: one with an AI crawler setting, or one whose firewall
has a rule on bots. Every other route runs exactly as before and counts nothing.

### What a route lets AI crawlers do

GPTBot, ClaudeBot, CCBot, Bytespider, PerplexityBot and the others, by their user agent. Pick in the route
dialog under **Who gets in**, in the **Bots** section of this page, or with `--ai-crawlers`:

| Setting | What happens |
| - | - |
| **Allow** (the default) | Nothing looks at them |
| **Block** | They get the `403` page, logged as `block ai:GPTBot` |
| **As robots.txt says** | Each crawler only where the site's own `robots.txt` lets it in; elsewhere the `403` page, logged as `block robots:GPTBot` |

For **As robots.txt says**, the node reads `/robots.txt` from the site's server after a crawler's first
visit, never while a visit waits, and keeps it for an hour.

> [!WARNING]
>
> A crawler is let in until the file is known, and whenever it can't be read: a 404, a server error or a
> timeout all count as *no rules* for ten minutes. So a crawler's first visit to a hostname always gets
> through. To keep it out from the first request, use **Block**.

`/robots.txt` itself stays open to crawlers, in **Block** too, so a polite one can read that it isn't
welcome. A firewall rule that allows the visit first wins over the setting.

A route with a setting other than **Allow** looks at every request's user agent: about 3 µs of CPU on one CPU
([Proxy benchmarks](https://docs.dnzwithazee.com/proxy/benchmarks.md#what-each-feature-costs)). **Allow** costs nothing.

> [!NOTE]
>
> **Google-Extended** and **Applebot-Extended** are only words in `robots.txt`: their requests say Googlebot
> and Applebot, so no route can refuse them. Write them in your `robots.txt`.

### Verified bots

A visitor claiming Googlebot, Bingbot, Applebot, DuckDuckBot, YandexBot, Baiduspider, UptimeRobot or Pingdom
is first treated as a **plain visitor**. DNZ checks the address in the background, against the ranges the
vendor publishes or by forward-confirmed reverse DNS (the reverse name must be in the vendor's domains, such as
`*.googlebot.com`, and that name must point back to the address). The verdict holds for a day: **verified** or
**fake**, and a fake one's log line says `fake bot`. None of it holds up a request.

If the check can't finish (a resolver that fails, a list that can't be read), there is no verdict and it stays a
plain visitor: a DNS failure never makes a real crawler fake. The same goes for a claim from a private address,
which is usually a forwarder you haven't told the entry point to trust.

The names ship with DNZ, from each vendor's own documentation, and reach the nodes only while a route or
firewall of that proxy uses them. **Names DNZ knows** in the section lists them all.

### Rules on bots

Add a rule with the condition **Bot** to a firewall in **Proxy → Middlewares**. It takes *verified*, *fake* and
*AI crawler*, with any action:

```json theme={null}
{ "name": "fakes", "action": "challenge", "conditions": [{ "field": "bot", "op": "in", "values": ["fake"] }] }
```

A visitor with no verdict yet is none of them, so its first visits pass. A node refuses a firewall with a field
it doesn't know, and the routes using it: so a rule on bots and an AI crawler setting are refused while a node
older than DNZ 2.5 serves that proxy, naming the node to update. Update a node before it joins.

### The section

**Humans** here means what is left of the requests told apart: people, bots DNZ doesn't know, and visitors whose
claim hasn't been checked yet. Routes that don't tell bots apart count in none of these. Per hostname you see
what its routes let AI crawlers do, and can change it there or for every route; each change is a version in
[route history](https://docs.dnzwithazee.com/proxy/route-history.md). Click a fake bot's address for its details.

## Preview a higher sensitivity

*What would this route block at High?* Let it run the extra rules in shadow mode for a while before you
raise it. A preview is off by default, per route, and turns itself off after 24 hours.

Start one with **Preview High** in the route dialog's WAF group, under **Previews of a higher
sensitivity** on this page, or in a batch edit. While it runs, the rules above the route's sensitivity
only count, with the note `shadow waf:<rule>`:

* the visitor gets exactly what they got before;
* nothing shows as blocked or logged: not in the totals, the map, the chart, the attack alerts or a SIEM's
  security events;
* no address gets closer to a [ban](#bans-that-expire).

The panel says what came of it in words:

> So far High would also block 37 requests: 30 from addresses the WAF stopped nothing else from (mostly
> /search), which may be real visitors, and 7 from addresses it also stopped.

Look at the first number before you switch: those may be real visitors. Only the busiest addresses are told
apart, so the second number is *at least*. The events' **Preview only** filter shows the requests themselves,
from the log's sample. **Switch to High** raises the route and ends the preview, and
[route history](https://docs.dnzwithazee.com/proxy/route-history.md) keeps a version.

The end is a time kept with the route: neither a restart nor DNZ being away extends it, since each node reads
the time itself. A node too old for previews ignores it, and the panel names it.

> [!NOTE]
>
> While it runs, a preview costs that route's requests one more pass over the extra rules. Measured on one
> CPU, a preview of High on a route at Medium cost a browser-like request about 5.5 µs (6% fewer requests a
> second at the most a node handles) and a plain one about 2 µs. A route without a preview pays nothing more.

## The weekly report

Every Monday at 08:00 in the server's time zone, DNZ sends the seven days before it through the
[notification providers](https://docs.dnzwithazee.com/integrations/notifications.md) that ticked **Weekly WAF report**: requests blocked and
logged, the busiest countries, rules and addresses, new bans, how many routes have the WAF, and **the nodes that
didn't answer**, whose numbers are missing from it.

A new provider doesn't get it until you tick it. Only the server that leads a cluster sends it, never a
secondary. DNZ remembers which Monday it sent, so a restart or a new leader doesn't send it twice. A server that
was down at 08:00 still sends it, up to 48 hours late. **Send it now** on this page sends the last seven days as
a test, which doesn't count as Monday's.

## Try a firewall rule on past traffic

Before you save a [firewall](https://docs.dnzwithazee.com/proxy/dnz-proxy.md#firewall-dnzfirewall) rule, **Try on past traffic** in the rule
editor says what it would have matched: how many requests, from how many addresses, with examples. Pick the
last hour to the last 7 days. The rule is what's in the editor, unsaved: nothing is written anywhere.

It runs on the nodes, over each one's request log, with the code the firewall itself runs, so a result can't
differ from the real rule. For a firewall that routes run, it looks only at those routes' hostnames. What the
rules above it stop never reaches it, as in the firewall.

**The log is a sample, and the answer says so.** Each node keeps a part of what it serves (blocked requests get
ten times the room):

> This rule would have matched 340 of the 5,210 lines in the logs' sample, from 12 addresses. The logs hold
> 3.1% of the 168,000 requests served in this window, which comes to about 11,000 requests in all (an estimate).

The estimate scales each hour and hostname by the requests the node counted, which are exact. It is an estimate
and no more: a request the log keeps more often than others weighs differently.

**What the log can't answer is named, never guessed.** A log line holds the host, method, path with its query
string, address, country, user agent and referer.

| Condition | What happens |
| - | - |
| Any other header | The line is a *maybe*, counted apart: *Another 12 match only if the X-Env header also holds*. With *all* conditions, a line another condition rules out isn't one; with *any*, one another condition catches is a real match |
| Country, on a node without a GeoIP database | A *maybe* too, and it says so |
| Network lists | Read as they are now, not as they were on the day of the request |
| Paths longer than 1,024 characters | Cut in the log; the answer counts those lines. User agents are kept up to 256 and referers up to 512 |

On a route that rewrites paths, a line may hold the rewritten path. Lines that were already blocked or challenged
count too, and are said apart.

It runs off the request path: one at a time on each node, at most 50,000 lines per node unless you ask for more
(200,000 at most), newest first, within 20 seconds. If it stops early the answer says where. A node too old to try
a rule isn't asked and is named, as is one that didn't answer. WAF exceptions and rate limits can't be tried this
way yet.

## When it wasn't an attack

When the WAF stops a real visitor or app, use **Not an attack…** on its event and pick the narrowest
exception:

| Exception | When |
| - | - |
| **Skip the rule** | One rule trips on a normal request of the site; the other rules keep working |
| **Leave the path alone** | A form or API sends code-like text on purpose; no rule looks at that path or below it |
| **Trust the address** | Your office, a scanner you run, a partner's server |

It goes into the route's [WAF exceptions](https://docs.dnzwithazee.com/proxy/waf.md#exceptions), and
[route history](https://docs.dnzwithazee.com/proxy/route-history.md) keeps a version, so you can undo it there. When a
[shield](https://docs.dnzwithazee.com/proxy/shields.md) stopped it, the one choice is to turn that shield off on those routes.

## Location data

* **Countries on the map** are counted on the nodes. Set a **GeoIP database URL** in the proxy's DNZ
  Proxy settings, as for country rules in the firewall. Without one, DNZ places the busiest
  addresses with its own country database and says so above the map.
* **Places and networks of addresses** come from DNZ's own country and ASN databases. **Download**
  fetches DB-IP's free *IP to Country Lite* and *IP to ASN Lite* (CC BY 4.0); DNZ downloads them
  again every month. MaxMind GeoLite2 works too.

## From the command line

```bash theme={null}
dnz proxy waf overview --since 7d                  # totals, attack types, rules, countries, addresses, hostnames
dnz proxy waf events --action block --country CN   # each request it matched, newest first
dnz proxy waf address 203.0.113.9                  # place, network, reverse name, what it set off
dnz proxy waf block 203.0.113.9 --host shop.example.com
dnz proxy waf block 203.0.113.0/24 --everywhere
dnz proxy waf unblock 203.0.113.0/24
dnz proxy waf except shop.example.com --skip-path /search
dnz proxy waf coverage                             # routes with and without the WAF
dnz proxy waf locations --download                 # DNZ's country and ASN databases
dnz proxy settings edge --bans on --ban-blocks 20 --ban-within 5 --ban-minutes 60 --ban-scope proxy
dnz proxy waf bans                                 # who is banned now
dnz proxy waf ban 203.0.113.9 --for 1h             # by hand; --proxy edge for one proxy
dnz proxy waf unban 203.0.113.9
dnz proxy waf lists                                # the lists, their size and last update
dnz proxy waf lists --on tor-exit                  # keep one up to date; --off, --download
dnz proxy waf campaigns --since 24h                # attacks grouped by network
dnz proxy waf campaign-block AS4134 --host shop.example.com   # its addresses; --ranges for its /24s
dnz proxy routes edit edge shop.example.com --ai-crawlers block   # or robots, allow
dnz proxy bots --since 7d                          # humans, verified bots, AI crawlers, fake bots
dnz proxy bots --list                              # the names DNZ knows and how each is checked
dnz proxy routes edit edge shop.example.com --waf-preview high   # 24 hours; --waf-preview-hours 6, --waf-preview off
dnz proxy waf previews                             # what each preview would also have stopped, in words
dnz proxy waf events --action shadow               # those requests, from the log's sample
dnz proxy waf report                               # what the weekly report says of the last seven days
dnz proxy waf report --send-now                    # sends it now to the providers that ticked it
dnz proxy firewall try office --number 2           # a firewall's rule on past traffic; nothing is saved
dnz proxy firewall try --rule '{"action":"block","conditions":[{"field":"path","op":"startsWith","values":["/admin"]}]}'
```

AI assistants get the MCP tools `proxy_waf_overview`, `proxy_waf_events`, `proxy_waf_address`,
`proxy_waf_coverage`, `proxy_waf_bans`, `proxy_waf_lists`, `proxy_waf_leaked_passwords`, `proxy_waf_campaigns` and, with write
access, `waf_block`, `waf_unblock`, `waf_exception`, `waf_ban` and `waf_unban`. Downloading lists
and location databases is left to people. `proxy_bot_traffic` reads the Bots section and, with write
`proxy_waf_coverage`, `proxy_waf_bans`, `proxy_waf_lists`, `proxy_waf_campaigns`, `proxy_waf_previews`,
`proxy_waf_report`, `try_firewall_rule` and, with write access, `waf_block`, `waf_unblock`, `waf_exception`, `waf_ban` and
`waf_unban`; `set_waf` starts a preview. Downloading lists and location databases and sending the report
are left to people. `proxy_bot_traffic` reads the Bots section and, with write
access, `set_ai_crawlers` sets what AI crawlers may do.
dnz proxy waf shields --since 7d                   # each shield's CVE, routes and hits; routes worth a look

```

AI assistants get the MCP tools `proxy_waf_overview`, `proxy_waf_events`, `proxy_waf_address`,
`proxy_waf_coverage`, `proxy_waf_shields`, `proxy_waf_bans`, `proxy_waf_lists`, `proxy_waf_campaigns`
and, with write access, `waf_block`, `waf_unblock`, `waf_exception`, `waf_ban`, `waf_unban` and
`set_shields`. Downloading lists
and location databases is left to people.
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.