---
url: 'https://docs.smoxy.eu/zones/waf-rules.md'
---
# WAF Rules

**WAF Rules** are the programmable half of smoxy's [security layer](/zones/security-and-waf). Where the managed WAF and the [managed scenarios](/zones/security-and-waf#managed-security-scenarios) decide for you, a WAF rule is yours: you describe which part of a request to inspect, how to compare it, and what the edge should do when it matches — block, challenge, throttle, skip, or just raise the request's anomaly score.

Rules are evaluated at the edge on every request, in the same pass as smoxy's own platform rules. A saved change is live within moments.

::: info
**Beta:** The rule engine is in beta. Platform-managed rules apply to every zone, and details of rule behavior may still change.
:::

### Where WAF Rules sit

The rule engine is the **last** stage of the security layer. Everything that can exempt a request has already run by the time a rule is evaluated:

That order has two practical consequences:

* An **allowlisted IP**, a **verified search-engine crawler**, or an Access Rule with **Allow** or **Skip (waf)** exempts the request before the engine runs. No WAF rule will see it. Use this deliberately for monitoring tools and trusted integrations.
* **Basic Auth runs after** the engine and is never skipped by a WAF rule. A `skip` rule does not hand out a password.

See the [Request Lifecycle](/concepts/request-lifecycle) for the full picture.

### Two kinds of rules

The **WAF Rules** card on the zone's WAF page holds two lists:

| List                     | Who owns it | What you can do                                                          |
| ------------------------ | ----------- | ------------------------------------------------------------------------ |
| **Custom rules**         | You         | Create, edit, delete, enable/disable                                     |
| **Smoxy-managed rules**  | smoxy       | Turn each one on or off **for this zone** — the rule itself is read-only |

Both lists are merged into a single evaluation list at the edge and sorted by **Order**, so a custom rule at order `0` is evaluated before a managed rule at order `10`. Managed rules are covered in [smoxy-managed rules](#smoxy-managed-rules).

### Rule budget

A zone gets **3 custom WAF rules** by default. The card shows a **WAF rule usage** meter (`2 / 3`, "1 rule remaining", "Limit reached"). Contact support if your zone needs more.

The budget covers custom rules only. Managed rules are unlimited and do not count against it, and it is separate from the shared rule budget used by [Access, Rewrite and Conditional Rules](/rules/rules-overview).

***

### Creating a rule

1. Open the zone's **WAF** page (Security → WAF).
2. In the **WAF Rules** card, click **Add rule**.
3. Fill in the description, conditions and action.
4. Click **Add rule** to save.

#### Rule fields

| Field           | Meaning                                                                                                             |
| --------------- | ------------------------------------------------------------------------------------------------------------------- |
| **Description** | A label, up to 255 characters. It is what the rule list shows and what appears in support conversations. Never evaluated. |
| **Enabled**     | Off keeps the rule but takes it out of evaluation entirely — it matches nothing and adds no score.                    |
| **Phase**       | **Request** — the rule is evaluated before the request goes upstream.                                                  |
| **Match**       | **All conditions** (AND) or **Any condition** (OR). Default: all.                                                     |
| **Order**       | Ascending. Lower runs first. Give every rule a distinct number — rules sharing an order have no defined relative order. |
| **Conditions**  | One or more condition rows: targets, an operator, a pattern, optional transforms, optional negate.                    |
| **Action**      | What happens on a match, plus the parameters that action needs.                                                       |

**Order** is a plain number you set yourself. Leaving gaps (10, 20, 30) makes it easy to slot a rule in later.

***

### Conditions

A condition asks one question about the request: *take these parts of the request, optionally clean them up, then test them with this operator against this pattern.*

#### Targets — what gets inspected

A target yields a **list** of values. Multi-valued targets fan out, and the operator runs against each value independently — which is why sprinkling extra query parameters does not help an attacker.

| Target                  | Yields                          | Notes                                                                                                     |
| ----------------------- | ------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `uri`                   | The request path                | Already percent-decoded and normalized. Query string excluded.                                              |
| `uri.raw`               | Path **and** query, verbatim    | Exactly as the client sent it, undecoded. The one target that survives encoding tricks — see the [first example](#example-1-a-file-probe-hidden-in-a-tracking-parameter). |
| `query`                 | The raw query string            | Everything after `?`, still URL-encoded, as one value.                                                       |
| `args`                  | Every query-parameter **value** | Decoded. Repeated parameters fan out. Query string only — form fields are not here.                          |
| `args.names`            | Every query-parameter **name**  | Catches parameter-name probes and prototype-pollution attempts.                                              |
| `headers`               | Every request-header value      | All of them. A repeated header fans out.                                                                     |
| `headers.user-agent`    | That header's value             |                                                                                                             |
| `headers.referer`       | That header's value             |                                                                                                             |
| `headers.host`          | That header's value             |                                                                                                             |
| `headers.content-type`  | That header's value             |                                                                                                             |
| `cookies`               | Every cookie value              | Cookie **names** are not inspectable.                                                                        |
| `body.raw`              | The whole request body          | One value, any content type — including multipart, which is otherwise unparsed.                              |
| `body.form`             | Every form-field value          | Only when the content type is `x-www-form-urlencoded`.                                                       |
| `body.json`             | Every leaf value in the JSON    | Recursive, max depth 32. Only when the content type contains `json`. Keys are not extracted, only leaves.     |
| `ip`                    | The client IP                   | The real visitor address, not a proxy hop. See [Client IP](/developer-guide/client-ip).                      |
| `method`                | The HTTP method                 | Uppercase, e.g. `POST`.                                                                                      |
| `host`                  | The request host                |                                                                                                             |
| `geoip.country`         | ISO country code                | Empty when unresolvable.                                                                                     |
| `geoip.asn`             | AS number                       | Numeric. `0` when unknown. Pair with a numeric operator or `within`.                                          |

A condition may list **several targets**. It is true when any value of any listed target passes the operator.

::: info
**Request bodies are inspected conditionally.** A `body.*` target sees data only when the method is `POST`, `PUT`, `PATCH` or `DELETE`, the `Content-Length` is **256 KB or less**, and the body stayed in memory. A large upload is skipped silently, so do not write a body rule expecting it to cover file uploads.
:::

#### Transforms — the anti-evasion layer

Transforms run on each extracted value, **left to right in the order listed**, before the operator sees it. Attackers evade with encoding far more often than with novel payloads, so this is where most real robustness comes from.

| Transform            | What it does                                                                                                                | Example                          |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
| `lowercase`          | Lowercases the value. ASCII-reliable.                                                                                        | `UNION` → `union`                |
| `uppercase`          | Uppercases the value.                                                                                                        | `abc` → `ABC`                    |
| `urlDecode`          | Percent-decodes `%XX` and turns `+` into a space.                                                                            | `%2e%2e%2f` → `../`              |
| `urlDecodeUni`       | Decodes IIS-style `%uXXXX` first, then percent-decodes. Codepoints ≥ 256 are dropped.                                        | `%u003cscript` → `<script`       |
| `htmlEntityDecode`   | Decodes `&#NN;`, `&#xHH;` and the named entities `lt gt amp quot apos nbsp`. Unknown names are left alone.                    | `&lt;script&gt;` → `<script>`    |
| `base64Decode`       | Decodes base64. If decoding fails the original value passes through unchanged, so it is safe to apply speculatively.          | `c2VsZWN0` → `select`            |
| `normalizePath`      | Collapses repeated slashes and resolves `.` / `..` segments. Does **not** decode first — put `urlDecode` before it.           | `/a/./b/../c` → `/a/c`           |
| `removeNulls`        | Strips `\0` bytes, the classic filter-splitting trick.                                                                       | `ad\0min` → `admin`              |
| `removeWhitespace`   | Strips **every** whitespace character.                                                                                       | `UNION  SELECT` → `UNIONSELECT`  |
| `compressWhitespace` | Collapses each run of whitespace to a single space, keeping word boundaries.                                                 | `UNION   SELECT` → `UNION SELECT` |
| `length`             | Replaces the value with its character count. Pair it with a numeric operator. On a multi-valued target this is the length of *each value*, not the number of values. | `abcd` → `4` |

::: tip
**A sane default stack.** For content inspection on `args`, `body.*` or `uri`: `urlDecode` → `removeNulls` → `lowercase`. Add `htmlEntityDecode` for XSS work and `normalizePath` for path-traversal work. Order matters: decode before you normalize, normalize before you lowercase.
:::

#### Operators — how the comparison is made

| Operator (UI label)          | Value                | Semantics                                                                                                        | Pattern format                                   |
| ---------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------ |
| **Matches regex**            | `rx`                 | PCRE regular expression, unanchored partial match. **Case-sensitive** unless you write `(?i)` or add `lowercase`.  | `(?i)union\s+select`                             |
| **Detect SQL injection**     | `detectSQLi`         | libinjection's SQL-injection detector — the same library the major open-source WAFs use.                            | *none*                                           |
| **Detect XSS**               | `detectXSS`          | libinjection's XSS detector.                                                                                        | *none*                                           |
| **Contains**                 | `contains`           | Literal substring search, no regex interpretation. Case-sensitive.                                                  | Plain string                                     |
| **Equals (string)**          | `streq`              | Exact equality. Case-sensitive.                                                                                     | Plain string                                     |
| **Begins with**              | `beginsWith`         | Literal prefix test.                                                                                                | Plain string                                     |
| **Ends with**                | `endsWith`           | Literal suffix test. An empty pattern never matches.                                                                | Plain string                                     |
| **Matches any phrase**       | `pm`                 | The value **contains** any one of the literals. Built for long keyword lists.                                       | Whitespace-separated: `sqlmap nikto nmap`        |
| **Is within set**            | `within`             | The value **equals** one member of the set.                                                                         | Whitespace- or comma-separated: `GET, HEAD`      |
| **IP matches**               | `ipMatch`            | Parses the value as an IP and tests it against a set of addresses and CIDRs. IPv4 and IPv6.                          | `10.0.0.0/8, 2001:db8::/32`                      |
| **≥  >  ≤  <  = (numeric)** | `ge` `gt` `le` `lt` `eq` | Both sides are read as numbers; a non-numeric value on either side is simply false. Pair with `length` or `geoip.asn`. | A number, e.g. `2048`                     |

::: warning
**The regex case trap.** `Matches regex` does not force case-insensitivity — the rule gets to choose. Nearly every false negative in a hand-written rule comes from writing `union select` and being defeated by `UnIoN SeLeCt`. Write `(?i)union\s+select`, or add the `lowercase` transform.
:::

A malformed pattern never errors the request: the condition simply evaluates to false.

#### Combining conditions

**Match** decides how the condition rows combine:

* **All conditions** — every row must be true (AND).
* **Any condition** — at least one row must be true (OR).

There is no nesting and no per-row operator: one flat list, one combinator. For anything more complex, split it across two rules.

::: warning
**Negate inverts the whole condition, not each value.** On a multi-valued target, **Negate** means *no value matched* — not *some value did not match*. A negated condition on `args` is false as soon as a **single** parameter matches.
:::

That behavior is also the only way to test for **absence**. A target that resolves to nothing yields an empty list, so nothing can pass the operator and the condition is false; with **Negate** on, it becomes true:

```
Targets:  headers.referer
Operator: Matches regex
Pattern:  .
Negate:   on
→ true when the request carries no Referer header at all
```

***

### Actions

| Action        | At the edge                                                                                                                    | Ends the request?      | Needs                   |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ----------------------- |
| **Block**     | Serves the zone's [security page](/zones/custom-pages) with `403` and stops.                                                     | yes                    | Response status         |
| **Challenge** | Serves the JavaScript proof-of-work interstitial. A client that already holds a valid pass goes straight through.                | unless the pass is valid | —                     |
| **Throttle**  | Slows the request down without rejecting it. The request continues to cache and origin.                                          | no                     | Rate and/or Delay       |
| **Skip**      | Takes the decision for this request so **no later WAF rule can act**, and records `skip`.                                        | no                     | —                       |
| **Log**       | Records the match and takes no action — but also takes the decision. See the warning below.                                      | no                     | —                       |
| **Score**     | Adds to the request's anomaly score and lets evaluation continue. The only genuinely passive action.                             | no                     | Score                   |

#### Block

A blocked request is answered with **`403`** and the zone's security page, which you can brand under [Custom Pages](/zones/custom-pages).

#### Score and the anomaly threshold

`Score` does not decide anything on its own. It adds its value to a running total for the request, and if **no rule took the decision** and the total reaches **5**, the request is blocked. The threshold is fixed at 5 and is not configurable per zone.

This is the tool for signals that are suspicious but not conclusive on their own — a scripted user agent, a missing `Accept-Language`, an odd method. None of them justifies a block; three of them together might.

Score is added for **every** action, not just `Score`. A blocking rule can carry a score as well, so the match still registers in the request's total.

#### Throttle

Throttle has two levers, and they hurt different abusers:

* **Rate (bytes/s)** caps response-body throughput. This is the lever for bandwidth-heavy scrapers. `0` means no cap.
* **Delay (ms)** adds a one-off pause before the request reaches cache or origin, capped at 5000 ms. This is the lever for many-small-request bots, where a tiny response body makes a rate cap meaningless.

At least one of the two must be non-zero. If several sources throttle the same request, the strongest wins: the largest delay and the smallest non-zero rate.

#### Skip

`Skip` exempts a request from the **rest of the WAF rule list** — including the smoxy-managed rules that would otherwise fire. Give it a low **Order** so it runs first.

It does not undo the earlier security stages. Reputation verdicts, Under Attack Mode and the IP lists have already been applied by the time the engine runs; to exempt a request from those, use an [Access Rule](/rules/access-rules) with **Allow** or **Skip**.

::: warning
**`Log` is not passive.** It records the match and takes no action of its own, but it *does* take the decision — so every later rule that would have blocked never gets to decide, and the anomaly threshold is skipped too.

For passive observation that leaves later rules free to act, use **Score** with a score of **0**. The match is still recorded and still visible in the traffic log.
:::

***

### How a request is decided

The outcome is decided by **order**, not by severity. A `challenge` at order 10 beats a `block` at order 20.

1. Your custom rules and the smoxy-managed rules are merged into **one** list and sorted by **Order** ascending.
2. Each rule is evaluated in turn. Disabled rules are skipped.
3. On a match, the rule's **score** is added to the running total — for any action, not just `Score`.
4. The **first** matching rule whose action is not `Score` takes the single decision slot. Later rules can still add score, but they can no longer change the decision.
5. After the list, if nothing took the decision and the accumulated score has reached **5**, the request is blocked.
6. The verdict is recorded on every request, matched or not — see [Checking that a rule fired](#checking-that-a-rule-fired).

So: **exactly one rule decides.** Everything else either contributes score or is too late.

***

### smoxy-managed rules

Rules under **Smoxy-managed rules** are written and maintained by smoxy and apply across the platform — things like blocking config-file harvesting, challenging Tor exit nodes, or throttling aggressive crawlers. You cannot edit them; you can only turn each one on or off for your zone with the row's switch.

Each row carries tags that tell you how the rule reaches your zone:

* **Managed** — the rule is smoxy's, not yours.
* **Opt-in** — the rule is **off** for your zone until you switch it on. Rules without this tag are on by default and stay on unless you switch them off.

Your explicit choice always wins over the default, and it is remembered: if smoxy later changes a rule's default, a zone that already made a choice keeps it.

#### Turning a managed rule off

Switch the row off. The rule stops enforcing for your zone immediately — it no longer blocks, challenges or throttles, and it no longer feeds the anomaly threshold.

It does keep being **evaluated**. A rule that is off for your zone contributes to a separate *shadow* score that appears in the traffic log with `waf_rule_action = shadow`, so both you and smoxy can see whether it *would* have fired. That makes an opt-out reversible on evidence rather than a guess.

::: tip
Leave managed rules on unless one is demonstrably causing false positives for your application. If you are unsure, look at the traffic log first, or contact support.
:::

***

### Checking that a rule fired

Every request carries the rule-engine verdict, whether or not anything matched. The values are captured in the zone's traffic log, and are available as response headers when [debug headers](/zones/proxy) are enabled for the zone.

| Response header      | Traffic-log field   | Meaning                                                                                    |
| -------------------- | ------------------- | -------------------------------------------------------------------------------------------- |
| `s-waf-rule`         | `waf_rule`          | The id of the rule that decided, or `score` when the anomaly threshold fired.                |
| `s-waf-rule-action`  | `waf_rule_action`   | `block`, `challenge`, `throttle`, `skip`, `log`, or `shadow`.                                |
| `s-waf-rule-score`   | `waf_rule_score`    | The request's anomaly score, including the shadow score from rules that did not enforce.     |

::: warning
**Two verdicts, similar names.** `waf_action` and `waf_score` are **not** the rule engine — they carry the IP-reputation verdict from the [managed scenarios](/zones/security-and-waf#managed-security-scenarios). A request blocked by one of your WAF rules routinely shows `waf_action: allow` right next to `waf_rule_action: block`.

When you are investigating a WAF rule, read **`waf_rule_action`**.
:::

`s-allowlist` tells you the opposite story: it names the trust list that exempted a request from the engine entirely (`global`, `tenant`, or `verified_bot`). If a rule you expected to fire did not, check this first.

***

### Examples

#### Example 1: a file probe hidden in a tracking parameter

Taken from live traffic. A request looked like an ordinary product page:

```
/fahrrad-bekleidung/…/castelli-espresso-2-w-radtrikot-kurzarm-damen-kaufen.html
  ?srsltid=AfmBOoovzZW4ddo…jwWUE/.env.development
```

A `.env` probe was appended to the end of a legitimate Google `srsltid` parameter. The `uri` target would have missed it — `uri` stops at the `?`. This is what `uri.raw` is for.

```
Description: Block environment and config file probes
Match:       Any condition
Order:       10
Condition:   Targets    uri.raw
             Transforms urlDecode, lowercase
             Operator   Matches any phrase
             Pattern    .env .git/config wp-config.php .aws/credentials id_rsa
Action:      Block
```

#### Example 2: SQL injection, without writing regexes

`Detect SQL injection` needs no pattern — it runs a purpose-built detector over each value.

```
Description: Block SQL injection in query and form input
Match:       Any condition
Order:       20
Condition 1: Targets    args, body.form
             Transforms urlDecode, removeNulls
             Operator   Detect SQL injection
Condition 2: Targets    body.json
             Transforms removeNulls
             Operator   Detect SQL injection
Action:      Block
```

#### Example 3: score suspicious automation instead of blocking it

Neither signal justifies a block on its own. Together with anything else scoring, they cross the threshold.

```
Description: Raise anomaly score for scripted clients
Match:       Any condition
Order:       200
Condition 1: Targets    headers.user-agent
             Transforms lowercase
             Operator   Matches any phrase
             Pattern    curl wget python-requests go-http-client libwww-perl
Condition 2: Targets    headers.user-agent
             Operator   Matches regex
             Pattern    ^$
Action:      Score
Score:       3
```

#### Example 4: slow a scraper down without blocking it

```
Description: Throttle bulk catalogue crawling
Match:       All conditions
Order:       50
Condition 1: Targets    uri
             Operator   Begins with
             Pattern    /catalog/
Condition 2: Targets    geoip.asn
             Operator   Is within set
             Pattern    14061, 16509, 24940
Action:      Throttle
Rate:        65536
Delay:       250
```

Hosting-provider ASNs are a strong signal for a shop catalogue: real customers rarely browse from a datacenter.

#### Example 5: exempt an endpoint that trips the rules

A webhook receiver posting unusual payloads can look like an attack. Give the exemption a low order so it runs before anything else.

```
Description: Skip WAF rules for the payment webhook
Match:       All conditions
Order:       0
Condition 1: Targets    uri
             Transforms normalizePath, lowercase
             Operator   Equals (string)
             Pattern    /webhooks/payment
Condition 2: Targets    ip
             Operator   IP matches
             Pattern    203.0.113.0/24
Action:      Skip
```

Pair the path with a source check. A `skip` rule on the path alone is an open door with your address on it.

#### Example 6: block a vulnerable endpoint while you patch

A virtual patch buys you the time to deploy a real fix.

```
Description: CVE-2026-31887 — block order-code enumeration
Match:       All conditions
Order:       5
Condition 1: Targets    uri
             Transforms urlDecode, normalizePath, lowercase
             Operator   Begins with
             Pattern    /store-api/order
Condition 2: Targets    method
             Operator   Equals (string)
             Pattern    POST
Condition 3: Targets    body.json
             Operator   Matches regex
             Pattern    ^[a-f0-9]{32}$
Action:      Block
```

***

### Managing rules through the API

Every field in the editor is available on the [API](/api/), plus a few that the interface does not expose.

| Method   | Endpoint                                                 | Purpose                                    |
| -------- | -------------------------------------------------------- | ------------------------------------------ |
| `GET`    | `/api/zones/{zoneId}/security/waf-rules`                 | List the zone's custom rules               |
| `POST`   | `/api/zones/{zoneId}/security/waf-rules`                 | Create a rule                              |
| `GET`    | `/api/zones/{zoneId}/security/waf-rules/{id}`            | Read one rule                              |
| `PATCH`  | `/api/zones/{zoneId}/security/waf-rules/{id}`            | Update a rule                              |
| `DELETE` | `/api/zones/{zoneId}/security/waf-rules/{id}`            | Delete a rule                              |
| `GET`    | `/api/zones/{zoneId}/security/global-waf-rules`          | List the managed rules applied to the zone |
| `PATCH`  | `/api/zones/{zoneId}/security/global-waf-rules/{id}`     | Turn a managed rule on or off for the zone |

Creating the scripted-client rule from example 3:

```bash
curl -X POST "https://api.smoxy.eu/api/zones/412/security/waf-rules" \
  -H "Authorization: Bearer $SMOXY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Raise anomaly score for scripted clients",
    "enabled": true,
    "phase": "request",
    "order": 200,
    "match": "any",
    "conditions": [
      {
        "targets": ["headers.user-agent"],
        "transforms": ["lowercase"],
        "operator": "pm",
        "pattern": "curl wget python-requests go-http-client libwww-perl"
      },
      {
        "targets": ["headers.user-agent"],
        "operator": "rx",
        "pattern": "^$"
      }
    ],
    "action": "score",
    "score": 3
  }'
```

Turning a managed rule off for a zone:

```bash
curl -X PATCH "https://api.smoxy.eu/api/zones/412/security/global-waf-rules/550e8400-e29b-41d4-a716-446655440000" \
  -H "Authorization: Bearer $SMOXY_TOKEN" \
  -H "Content-Type: application/merge-patch+json" \
  -d '{"enabledForZone": false}'
```

See [API Tokens](/developer-guide/api-tokens) for authentication.

#### Fields the interface does not expose

| Field        | Effect                                                                                                                     |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| `stop`       | Ends rule evaluation entirely once this rule matches — later rules do not even add score. Ignored for managed rules that are off for the zone. |
| `expression` | An escape hatch for logic the condition list cannot express, using the same expression language as [Access Rules](/rules/access-rules). Evaluated **only** when `conditions` is empty. |

#### Validation

The API rejects a rule that fails any of these:

* At least one condition, or an `expression`.
* Every condition has a non-empty `targets` array and a known `operator`.
* Every operator except `detectSQLi` and `detectXSS` has a non-empty `pattern`.
* `action: block` carries a `status` between 100 and 599.
* `action: throttle` carries a non-zero `rateBps` and/or `delayMs`.
* Targets and transforms are from the lists on this page.

***

### Things to know

* **Order decides, not severity.** The first matching non-`Score` rule wins. Keep exemptions at the bottom of the number range and blocks above them, and give every rule a distinct order.
* **`Log` and `Skip` silence later rules.** Only `Score` is passive.
* **Regexes are case-sensitive** unless you say otherwise with `(?i)` or `lowercase`.
* **Decode before you compare.** A rule without transforms is one `%2e%2e%2f` away from being useless.
* **`uri` is normalized, `uri.raw` is not.** Inspect both when you are hunting for smuggled payloads.
* **Bodies over 256 KB are not inspected.** Neither are bodies on `GET`.
* **A rule cannot override an earlier decision.** Allowlisted addresses, verified crawlers and Access Rule exemptions never reach the engine.
* **Blocks answer `403`.** Brand the page under [Custom Pages](/zones/custom-pages).
* **Test with `Score: 0` first.** Ship a new rule passively, watch `waf_rule` in the traffic log for a day, then switch it to the action you actually want.

### Where to go next

* [Security & WAF](/zones/security-and-waf) — the surrounding security layer
* [Access Rules](/rules/access-rules) — the earlier, coarser policy layer
* [Threat Lookup](/zones/threat-lookup) — investigate a single address
* [Request Lifecycle](/concepts/request-lifecycle) — where every stage runs
