# Protect web applications from OWASP Top 10 and zero-day attacks

A security team protects a customer-facing web application that runs on its own origin, often behind a standalone WAF, appliances, or separate bot and DDoS vendors. Each of those tools holds its own policy, and the origin still answers anyone who finds its address. This page configures one firewall in front of the application, which refuses listed networks, scores every request with WAF and Bot Manager, and lets the origin accept connections from Azion only. The result is measured by the attacks blocked before the origin, the false-positive rate on legitimate traffic, and the time to change a policy.

This use case does not cover API-specific abuse, which [Protect public APIs from abuse](/en/documentation/use-cases/secure-applications-and-networks/protect-public-apis-from-abuse/) covers, or account takeover, which [Block account takeover on login and checkout flows](/en/documentation/use-cases/secure-applications-and-networks/block-account-takeover-on-login-and-checkout-flows/) covers.

## Prerequisites

- An application that serves the web application through a connector and a workload. To create them, refer to [Applications quickstart](/en/documentation/platform/applications/quickstart/).
- A firewall bound to that workload's deployment. To bind one, refer to [Bind a firewall to a workload](/en/documentation/guides/application-security/firewall-and-waf/firewall-protect-your-domain/).
- WAF and Functions turned on in the firewall's **Main Settings** › **Modules**. A new firewall carries WAF off. To turn it on, refer to [Set a firewall's main settings](/en/documentation/guides/application-security/firewall-and-waf/firewall-configure-main-settings/).
- Bot Manager Lite installed from Azion Marketplace, or Bot Manager enabled on the account. To install Bot Manager Lite, refer to [Install Bot Manager Lite](/en/documentation/guides/application-development/integrations/bot-manager-lite/).
- Access to the firewall of your origin, where the allowlist of Azion's addresses goes.
- A personal token, for the API tabs. To create one, refer to [Personal tokens](/en/documentation/guides/platform/account-and-billing/personal-tokens/).
- The values of your application. This page uses `www.example.com` for the domain, `198.51.100.7` for an address your team already blocks, and `webapp` as the prefix of every object it creates. Replace each value with yours in every step.

---

## Required products

| The application needs                                                      | Which means                                                                                             | Product          | Documented in                                                                                                                                                    |
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Sources the team already distrusts refused before any inspection           | A network list that a deny rule reads through the *Network* criterion                                   | Network Shield   | [Network Lists](/en/documentation/platform/firewall/network-shield/network-lists/)                                                                               |
| Requests that carry OWASP Top 10 attack patterns refused before the origin | A WAF rule set that a rule's *Set WAF* behavior applies to every request                                | WAF              | [WAF quickstart](/en/documentation/platform/firewall/waf/quickstart/)                                                                                            |
| Automated clients told apart from people                                   | A Bot Manager function instance that a *Run Function* rule runs on every page request                   | Bot Manager      | [Run Bot Manager on selected paths](/en/documentation/guides/application-security/bots-and-network/run-bot-manager-on-selected-paths/)                           |
| An origin that accepts connections only from Azion                         | Origin IP ACL on the connector, and the `Azion Origin Shield` prefixes allowed at the origin's firewall | Origin Shield    | [Restrict an origin to Azion with Origin IP ACL](/en/documentation/guides/application-security/bots-and-network/restrict-an-origin-to-azion-with-origin-ip-acl/) |
| Security events in the team's SIEM                                         | A stream of the *WAF Events* data source to the SIEM's endpoint                                         | Data Stream      | [Stream WAF events to a SIEM](/en/documentation/guides/application-security/firewall-and-waf/integrate-siems/)                                                   |
| Each block explained, request by request                                   | The WAF fields of the request's record, found by its `x-azion-request-id`                               | Real-Time Events | [Find the WAF score of a blocked request](/en/documentation/guides/application-security/firewall-and-waf/how-to-find-waf-score/)                                 |

DDoS Protection mitigates DoS and DDoS attacks on every workload, before any firewall rule runs, with nothing to create or configure. For the attack types it covers, refer to [Attack mitigation](/en/documentation/platform/workloads/ddos-protection/ddos-mitigation/).

---

## Reference architecture

This page builds the *Web application and API protection (WAAP) perimeter*: one firewall policy in front of the workload, with the origin closed to every other path.

```mermaid
%%{init: {"layout": "dagre", "themeVariables": {"fontSize": "13px"}, "flowchart": {"nodeSpacing": 12, "rankSpacing": 12, "padding": 6, "wrappingWidth": 70, "minNodeWidth": 40, "useMaxWidth": true}}}%%
flowchart TD
  Visitor["Visitor"] -->|"HTTPS request"| DDoS["DDoS Protection"]
  DDoS --> Net["rule 1: Network Shield lists"]
  Net -->|"listed source"| Deny["403"]
  Net -->|"not listed"| WAF["rule 2: WAF rule set"]
  WAF -->|"score at threshold, Blocking"| Bad["400"]
  WAF -->|"below threshold"| Bot["rule 3: Bot Manager instance"]
  Bot -->|"score below threshold"| App["application"]
  App --> Conn["connector with Origin IP ACL"]
  Conn --> Origin["origin: allows Azion prefixes only"]
  WAF -.->|"WAF Events"| Stream["Data Stream"]
  Stream --> SIEM["SIEM"]
```

Read the diagram from top to bottom. DDoS Protection acts before any rule, and the firewall's rules then decide each request in order. Network Shield, WAF, and Bot Manager each act only through the rule that calls them, so the order of those rules is the order of the policy. Every request that passes ends at one connector, and the origin's own firewall closes every other path to it. The dotted edge carries records, not requests: the WAF events leave for the SIEM through Data Stream.

### Dataflow

1. A visitor's request reaches the workload on `www.example.com`. DDoS Protection assesses it first, and the firewall bound to the workload takes it next.
2. The first rule compares the client address with the team's network list and with the Tor exit node list. A listed client receives `403`, and no later rule runs.
3. The second rule hands the request to the WAF rule set. In *Blocking*, a request whose score reaches a family's threshold receives `400`.
4. The third rule runs the Bot Manager instance on every request that is not a static asset. A score at the threshold runs the instance's action.
5. A request that no rule stops reaches the application, which forwards it to the origin through the connector. The origin's firewall accepts the connection because it comes from an `Azion Origin Shield` prefix, and refuses connections from anywhere else.
6. Data Stream sends each request WAF analyzed to the SIEM. Real-Time Events keeps the record of each request for investigation, joined to a refusal by its `x-azion-request-id`.

### Components

- **firewall**: the Platform Resource that enforces the policy. A workload's deployment names it, and it runs its rules on every request to that workload before the application sees it.
- **DDoS Protection**: the Feature that mitigates DoS and DDoS attacks on every workload, always on and with nothing to configure. It acts before any firewall rule.
- **WAF**: scores each request a *Set WAF* rule hands it against eight threat families, and refuses the request in *Blocking* mode when a score reaches its threshold. It filters OWASP Top 10 and other attack patterns.
- **Bot Manager**: scores each request a *Run Function* rule hands it for signs of automation, and runs the action its instance sets once the score reaches the threshold.
- **Network Shield**: adds the *Network* criterion, which matches the client address against a list of addresses, ASNs, or countries. It refuses known-bad sources before WAF and Bot Manager score them.
- **application**: the Platform Resource that delivers the requests the firewall lets through, and forwards them to the origin.
- **connector**: the Platform Resource that reaches the origin. Every request that passes the policy ends at it.
- **Origin Shield**: Origin IP ACL on the connector publishes the `Azion Origin Shield` list of Azion's prefixes, which the origin's firewall allows while it refuses every other source. No client can then reach the origin around the policy.
- **Data Stream**: sends the requests WAF analyzed, with their score, matched rules, and action, to an endpoint the SIEM reads.
- **SIEM**: the integration that correlates the firewall's events with the team's other security sources.
- **Real-Time Events**: holds the record of each request, with its WAF fields, so a block can be explained from the request ID its client reports.

---

## Configure the network blocks

The network rule runs first, so a source the team already distrusts is refused before WAF and Bot Manager score it. WAF is billed on the requests it scores, and Bot Manager on the requests it evaluates, so a request this rule denies costs neither. The rule reads two lists. `webapp-blocked-addresses` holds the addresses your team blocks. `Azion IP Tor Exit Nodes`, list `2`, is maintained by Azion, so the rule also matches the exit nodes Azion adds later.

The behavior is *Deny (403 Forbidden)* rather than *Drop*. A denied visitor sees a page with a request ID that a person blocked by mistake can report. Once the list refuses only the clients you expect, you can switch to *Drop (Close Without Response)*.

**Console**

To create the list:

1. **Open the Network Lists page**

   Access [Azion Console](https://console.azion.com/) > **Edge Libraries** > **Network Lists**.

2. **Select Network List**

3. **Name the list**

   In the **General** section, enter `webapp-blocked-addresses` as the **Name**.

4. **Select the IP/CIDR type**

   In the **Network List Settings** section, select *IP/CIDR*. The form opens with *ASN* selected.

5. **Enter the addresses**

   In the **List** field, enter `198.51.100.7 #blocked by the security team`, one address per line.

6. **Select Save**

To create the rule that reads it:

1. **Open the firewall bound to your workload**

   Access **Firewalls**, select the firewall, then go to the **Rules Engine** tab.

2. **Select Rule**

3. **Name the rule**

   Enter `webapp - deny listed networks`.

4. **Match the team's list**

   In the **Criteria** section, select the *Network* variable and the *matches* operator, then select `webapp-blocked-addresses` in **Select a Network**.

5. **Match the Tor exit nodes**

   Add a criterion joined by **Or**: *Network* *matches* `Azion IP Tor Exit Nodes`.

6. **In the Behaviors section, select Deny (403 Forbidden)**

7. **Select Save**

**API**

To create the list, send it to the network lists:

```bash
curl --request POST \
  --url https://api.azion.com/v4/workspace/network_lists \
  --header 'Authorization: Token <personal-token>' \
  --header 'Content-Type: application/json' \
  --data '{"name":"webapp-blocked-addresses","type":"ip_cidr","items":["198.51.100.7 #blocked by the security team"]}'
```

The API answers `201` with a `state` of `executed`. Keep the list's `id` for the rule:

```json
{"state":"executed","data":{"id":<network-list-id>,"name":"webapp-blocked-addresses","type":"ip_cidr","items":["198.51.100.7 #blocked by the security team"],...}}
```

To create the rule, send both lists in one block joined by `or`. The list IDs are JSON integers, and `2` is the Tor exit node list:

```bash
curl --request POST \
  --url https://api.azion.com/v4/workspace/firewalls/<firewall-id>/request_rules \
  --header 'Authorization: Token <personal-token>' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "webapp - deny listed networks",
  "active": true,
  "criteria": [
    [
      { "variable": "${network}", "conditional": "if", "operator": "is_in_list", "argument": <network-list-id> },
      { "variable": "${network}", "conditional": "or", "operator": "is_in_list", "argument": 2 }
    ]
  ],
  "behaviors": [{ "type": "deny" }]
}'
```

The API answers `202` with a `state` of `pending`, and the rule's `order`. Create this rule before the WAF and Bot Manager rules, so it holds the first position.

A client in either list receives `403` once the rule propagates, which takes 6 to 10 minutes for a new rule. A later change to the items of `webapp-blocked-addresses` reaches traffic in about 100 seconds, with no change to the rule.

---

## Configure the WAF rule set

The rule set `webapp-waf` scores each request against the eight threat families at `medium` sensitivity, the level every family starts at. The rule that applies it matches `${request_uri}` *starts with* `/`, which matches every request. A criterion on the query string would skip every `POST` that carries its payload in the body.

The rule starts in *Logging*. A request that reaches a threshold is recorded and still served, and those records are the only description of your traffic as the rule set sees it. Move the rule to *Blocking* once 3 days of **Tuning** hold no request that should have been served.

**Console**

To create the rule set:

1. **Open the WAF Rules page**

   Access [Azion Console](https://console.azion.com/) > **Edge Libraries** > **WAF Rules**.

2. **Select + WAF Rule**

3. **Name the rule set**

   In the **General** section, enter `webapp-waf` as the **Name**.

4. **Keep every family at Sensitivity Medium**

   The **Threat Type Configuration** section lists the eight threat families, each at *Sensitivity Medium*.

5. **Select Save**

To apply it:

1. **Open the firewall's Rules Engine tab**

   Access **Firewalls**, select the firewall, then go to the **Rules Engine** tab.

2. **Select + Rule**

3. **Name the rule**

   Enter `webapp - apply webapp-waf`.

4. **Match every request**

   In the **Criteria** section, select `Request Uri`, *starts with*, and `/`.

5. **Add the Set WAF behavior**

   In the **Behaviors** section, select **Set WAF**, then `webapp-waf` and *Logging*.

6. **Select Save**

**API**

To create the rule set, send the eight families at `medium`:

```bash
curl --request POST \
  --url https://api.azion.com/v4/workspace/wafs \
  --header 'Authorization: Token <personal-token>' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "webapp-waf",
  "active": true,
  "product_version": "1.0",
  "engine_settings": {
    "engine_version": "2021-Q3",
    "type": "score",
    "attributes": {
      "rulesets": [1],
      "thresholds": [
        { "threat": "cross_site_scripting", "sensitivity": "medium" },
        { "threat": "directory_traversal", "sensitivity": "medium" },
        { "threat": "evading_tricks", "sensitivity": "medium" },
        { "threat": "file_upload", "sensitivity": "medium" },
        { "threat": "identified_attack", "sensitivity": "medium" },
        { "threat": "remote_file_inclusion", "sensitivity": "medium" },
        { "threat": "sql_injection", "sensitivity": "medium" },
        { "threat": "unwanted_access", "sensitivity": "medium" }
      ]
    }
  }
}'
```

The API answers `202` with a `state` of `pending`. Keep the rule set's `id`:

```json
{"state":"pending","data":{"id":<waf-id>,"active":true,"name":"webapp-waf",...}}
```

To apply it, create the rule with `mode` set to `logging`. The `mode` is required:

```bash
curl --request POST \
  --url https://api.azion.com/v4/workspace/firewalls/<firewall-id>/request_rules \
  --header 'Authorization: Token <personal-token>' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "webapp - apply webapp-waf",
  "active": true,
  "criteria": [
    [{ "variable": "${request_uri}", "conditional": "if", "operator": "starts_with", "argument": "/" }]
  ],
  "behaviors": [{ "type": "set_waf", "attributes": { "waf_id": <waf-id>, "mode": "logging" } }]
}'
```

The API answers `202` with a `state` of `pending` and the rule as it was stored.

The rule set scores every request the network rule lets through, and records what would have been blocked. To read those records and turn a false positive into an exception, refer to [Tune a WAF rule set](/en/documentation/guides/application-security/firewall-and-waf/tune-waf/). To move the rule to *Blocking*, refer to [Switch a rule set to blocking](/en/documentation/guides/application-security/firewall-and-waf/switch-to-blocking/).

---

## Configure Bot Manager scoring

The Bot Manager instance starts in observation mode: `action` is `allow`, so it refuses nothing, and `internal_logs` is `2`, so every request writes a report line. Run it for 24 to 72 hours, long enough to cover peak hours, weekly crawlers, and overnight jobs. `threshold` is `18`, the value Bot Manager documents to start from. While `action` is `allow`, the threshold only sets the `classified` label of each line. `log_tag` is `webapp-observe`, so each line names this instance.

The rule that runs the instance excludes static assets, because an image or a stylesheet is not a client, and each one is a request Bot Manager bills. It names no path, so every other request of the application is scored.

Create the instance and the rule as [Run Bot Manager on selected paths](/en/documentation/guides/application-security/bots-and-network/run-bot-manager-on-selected-paths/) describes, with these values:

- **Instance**: `webapp-bot-observe`, with these arguments:

  ```json
  { "threshold": 18, "action": "allow", "internal_logs": 2, "log_tag": "webapp-observe" }
  ```

- **Rule**: `webapp - score page requests`, with one block of criteria: the static-asset exclusion, `Request Uri` *does not match* the guide's expression. The rule carries no block of paths.

- **Behavior**: *Run Function* with `webapp-bot-observe`.

Every page request is scored and served, and each one writes a report line tagged `webapp-observe`. When the window closes, read the scores, set `threshold` in the gap between the legitimate and the automated clusters, and set `action` to `deny`. For the procedure, refer to [Monitor and calibrate Bot Manager](/en/documentation/guides/application-security/bots-and-network/monitor-and-calibrate-bot-manager/).

---

## Configure origin allowlisting

Origin IP ACL on the connector gives the account the `Azion Origin Shield` network list, which holds every IPv4 and IPv6 prefix that Azion's infrastructure uses to connect to origins. Your origin's firewall then allows those prefixes and denies every other source, so a client that connects to the origin's address directly never reaches the application around the firewall policy above.

The allowlist is the procedure that [Restrict an origin to Azion with Origin IP ACL](/en/documentation/guides/application-security/bots-and-network/restrict-an-origin-to-azion-with-origin-ip-acl/) describes, run on the connector that reaches the web application's origin. The origin's firewall holds the IPv4 and the IPv6 prefixes of the list, and denies every other source.

---

## Verify the setup

Wait for the rules to propagate before you judge a result: a new rule reaches traffic 6 to 10 minutes after it is saved, and answers alternate until it settles. Repeat each request until the answer holds.

- **A listed source is refused before inspection.** From an address in `webapp-blocked-addresses`, request the home page:

  ```bash
  curl -s -o /dev/null -w '%{http_code}\n' https://www.example.com/
  ```

  The command prints `403`.

- **WAF scores an attack pattern.** From an address that is not listed, send an injection-shaped query string:

  ```bash
  curl -i "https://www.example.com/?q=1%27%20OR%20%271%27%3D%271"
  ```

  In *Logging*, the application answers as usual. After the switch to *Blocking*, the same request receives:

  ```text
  HTTP/2 400
  ```

  The response's `x-azion-request-id` finds the request in the `workloadEvents` dataset of Real-Time Events, where `wafMatch` names the internal rules that matched, `1009` and `1013` for this query string. For the lookup, refer to [Find the WAF score of a blocked request](/en/documentation/guides/application-security/firewall-and-waf/how-to-find-waf-score/).

- **Bot Manager scores page requests.** Send a request with no user agent, which is what a scripted client sends:

  ```bash
  curl -A "" https://www.example.com/
  ```

  The request is served, and the `functionConsoleEvents` dataset of Real-Time Events holds a line that opens with `[Bot-Protection][webapp-observe] Report:`, carrying its `score` and `matched_rules`. For the query, refer to the [Bot Manager quickstart](/en/documentation/platform/firewall/bot-manager/quickstart/).

- **Static assets are not scored.** Request an asset, such as `https://www.example.com/styles/main.css`. No report line tagged `webapp-observe` appears for it.

- **The origin refuses direct connections.** From a host outside Azion, connect to the origin's own address. The origin's firewall refuses the connection, while requests to `www.example.com` still reach the application.

- **Security events reach the SIEM.** In Real-Time Events, the *Data Stream* data source lists each send of the stream, and a **Status Code** of `200` means the SIEM's endpoint accepted the batch.

---

## Measuring results

| Metric                                    | Where to read it                                                                                                                                                                                                                                                | What working looks like                                                                             |
| ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| Attacks blocked before the origin         | **Threats vs Requests** on the WAF dashboard of Real-Time Metrics, which splits analyzed requests into blocked threats, logged threats, and allowed requests. Refer to [Secure dashboards](/en/documentation/platform/real-time-metrics/secure-dashboards/#waf) | After the switch to *Blocking*, the threats the rule set finds appear as blocked rather than logged |
| False-positive rate on legitimate traffic | The **Tuning** tab of `webapp-waf`, which lists the requests that reached a threshold over the last 3 days. Refer to [Tuning](/en/documentation/platform/firewall/waf/scoring-and-modes/#tuning)                                                                | No request you recognize as legitimate, and no exception older than the request that produced it    |
| Time to change a policy                   | The time from saving a change to the answer holding at a repeated request, as in [Wait for a change to propagate](/en/documentation/platform/firewall/best-practices/#wait-for-a-change-to-propagate-before-you-judge-it)                                       | A list change holds in about 100 seconds, and a new rule in 6 to 10 minutes                         |

---

## Best practices

- **Put the network rule first.** The firewall runs its rules in order, and no later rule runs for a request a deny stops. A listed source then never reaches WAF or Bot Manager, which both bill on the requests they see. For rule order, refer to [How Firewall works](/en/documentation/platform/firewall/how-it-works/#rule-order).
- **Change a list, not a rule, for a source that changes often.** A change to a list's items reaches traffic in about 100 seconds, a new rule in 6 to 10 minutes, and the list adds nothing to the rules your plan includes per firewall.
- **Keep the WAF rule's criterion on the request URI.** `${request_args}` *matches* `.*` reads as every request and skips every request without a query string. `${request_uri}` *starts with* `/` matches them all, as in [Match the request, not its query string](/en/documentation/platform/firewall/best-practices/#match-the-request-not-its-query-string).
- **Raise one threat family at a time.** Raising several families at once produces false positives together, with nothing to say which raise produced which block. For the steps, refer to [Raise the sensitivity of one threat family](/en/documentation/guides/application-security/firewall-and-waf/raise-one-threat-family/).
- **Keep IPv6 in the origin allowlist, and update it within 7 days.** The list carries IPv6 prefixes for every data center, and the servers behind a new prefix go into production 7 days after Azion publishes it. An allowlist that misses either refuses connections Azion opens. For the update, refer to [List updates](/en/documentation/platform/connectors/origin-shield/origin-ip-acl-and-hmac/#list-updates).

---

## Guides in this use case

- [Run Bot Manager on selected paths](/en/documentation/guides/application-security/bots-and-network/run-bot-manager-on-selected-paths.md): Create the observation instance and the rule that scores every request except static assets.
- [Restrict an origin to Azion with Origin IP ACL](/en/documentation/guides/application-security/bots-and-network/restrict-an-origin-to-azion-with-origin-ip-acl.md): Publish the Azion Origin Shield prefixes and allow only them at the origin's firewall.
