# Deploy an edge WAF with the edge-waf template

Source: https://docs.quake.ai/resources/deployments/deploy-edge-waf-template
Markdown: https://docs.quake.ai/resources/deployments/deploy-edge-waf-template.md

---

# Deploy an edge WAF with the edge-waf template

Stand up a [SafeLine](https://github.com/chaitin/SafeLine) L7 web application firewall on one Quake AI instance using the [validated OpenTofu template](/docs/platform/validation#how-infrastructure-templates-are-checked) `edge-waf`. You apply the template, launch a private backend on the same subnet, configure the protected site in the SafeLine dashboard, enable managed rules in blocking mode, confirm an injection probe is blocked, and lock the backend security group to the WAF.

The WAF holds the only public floating IP. The backend stays on the private subnet with no address on the internet. You operate both instances yourself; this is a regional L7 WAF you run, not a CDN or volumetric DDoS scrubber.

<Figure size="md" caption="What you'll build: SafeLine on a WAF instance with a floating IP inspects HTTP traffic and forwards clean requests to a private backend on the same subnet">

```d2
direction: right

client: Browser {shape: person}
fip: Floating IP\n80 / 443
waf: WAF VM\nSafeLine {
  engine: L7 inspection
}
backend: Backend VM\nno floating IP {
  app: App :8080
}

client -> fip: HTTP
fip -> waf.engine
waf.engine -> backend.app: private subnet only
```

</Figure>

<PricingCompanion
  components={[
    { kind: "template", slug: "edge-waf", required: true },
    { kind: "primitive", required: true, label: "Private backend instance", vm: { flavor: "s1a.small", count: 1 } },
  ]}
/>

## Prerequisites

You need:

- OpenTofu 1.6.0 or later (or Terraform 1.6.0 or later) installed locally.
- Your OpenStack credentials sourced into the shell (`source openrc.sh`). See [the OpenStack CLI guide](/docs/tools/openstack-cli).
- An SSH keypair that already exists in your project. Record its name for the `key_name` variable.
- A copy of the `edge-waf` template directory from [the template reference page](/resources/iac-templates/edge-waf).
- A domain you can point at the WAF floating IP when you enable TLS on the protected site (optional for the HTTP verification steps below).

## Step 1: Apply the WAF template

Copy the template's example variables file and set `key_name`. Keep the default `upstream_host` and `upstream_port` unless you plan a different private address:

```bash
cp terraform.tfvars.example terraform.tfvars
```

```hcl
key_name       = "YOUR_KEY_NAME"
upstream_host  = "10.42.0.10"
upstream_port  = 8080
```

Initialize, preview, and apply:

```bash
tofu init
tofu plan
tofu apply
```

OpenTofu provisions a private network, router, security group, data volume, WAF instance, and floating IP. cloud-init installs Docker, downloads the official SafeLine compose bundle, generates a Postgres password on first boot, and starts SafeLine.

Record the outputs:

```bash
tofu output floating_ip
tofu output private_ip
tofu output waf_url
tofu output dashboard_hint
```

Wait three to five minutes for cloud-init and SafeLine to finish before you open the dashboard.

## Step 2: Launch a private backend on the WAF network

The template creates network `edge-waf-net` and subnet `edge-waf-subnet` (names follow the default `app_name`). Launch a small backend instance on that subnet at the fixed address `10.42.0.10` with no floating IP.

1. Create a security group for the backend:

```bash
openstack security group create app-backend-sg \
  --description "Private app backend; ingress from WAF only after step 7"
```

Allow SSH from your workstation while you configure the backend (tighten or remove this rule after setup):

```bash
openstack security group rule create \
  --protocol tcp --dst-port 22 --remote-ip YOUR_IP/32 app-backend-sg
```

2. Create a port with the fixed private IP:

```bash
SUBNET_ID=$(openstack subnet list -f value -c ID -c Name | awk '/edge-waf-subnet/ {print $1}')

openstack port create \
  --network edge-waf-net \
  --fixed-ip subnet=$SUBNET_ID,ip-address=10.42.0.10 \
  --security-group app-backend-sg \
  app-backend-port
```

3. Write a short cloud-init file that serves a test page on port 8080:

```bash
cat > backend-cloud-init.yaml <<'EOF'
#cloud-config
package_update: true
packages:
  - nginx
runcmd:
  - |
    echo 'backend ok' > /var/www/html/index.html
    printf '%s\n' 'server {' '  listen 8080;' '  root /var/www/html;' '}' > /etc/nginx/sites-available/default
    systemctl restart nginx
EOF
```

4. Launch the backend instance on the port:

```bash
openstack server create \
  --flavor s1a.small \
  --image Ubuntu-24.04 \
  --key-name YOUR_KEY_NAME \
  --port app-backend-port \
  --user-data backend-cloud-init.yaml \
  app-backend
```

Wait until the instance reports `ACTIVE`, then confirm the backend answers on the private subnet from the WAF host:

```bash
ssh ubuntu@YOUR_FLOATING_IP 'curl -s http://10.42.0.10:8080/'
```

The response body should include `backend ok`.

## Step 3: Retrieve the SafeLine admin credentials

SafeLine does not ship admin credentials in the repository. SSH to the WAF instance and reset the one-time admin password:

```bash
ssh ubuntu@YOUR_FLOATING_IP 'docker exec safeline-mgt resetadmin'
```

Record the username and password the command prints.

## Step 4: Open the management dashboard

Port 9443 is not open in the WAF security group. Reach the dashboard through an SSH tunnel from your workstation:

```bash
ssh -L 9443:127.0.0.1:9443 ubuntu@YOUR_FLOATING_IP
```

In a browser on the same workstation, open `https://127.0.0.1:9443/` and sign in with the credentials from step 3. Accept the self-signed certificate warning for the local tunnel.

## Step 5: Configure the protected site and upstream

In the SafeLine dashboard, add a protected site that forwards to your private backend:

1. Go to **Websites** (or **Applications**) and select **Add website**.
2. Set the site address to your WAF floating IP for now (for example `http://YOUR_FLOATING_IP`), or to your domain when you already pointed DNS at the floating IP.
3. Set the upstream origin to `http://10.42.0.10:8080` (the private backend from step 2).
4. Save the site and wait until SafeLine reports it healthy.

From your workstation, request the WAF floating IP over HTTP:

```bash
curl -s http://YOUR_FLOATING_IP/
```

The response should show `backend ok`. Traffic flows client to WAF floating IP to SafeLine to the private backend.

If SafeLine returns `502`, the protected site may still be propagating or the backend is not ready. Retry after a minute. SSH to the WAF host and run `docker compose -f /data/safeline/compose.yaml ps` to confirm SafeLine containers are up.

## Step 6: Enable managed rules in blocking mode

In the SafeLine dashboard for the site you created:

1. Open **Protection** (or **Security policy**) for the site.
2. Enable the **Semantic analysis** or **Managed rules** bundle (the built-in attack-signature set SafeLine ships for community edition).
3. Set the action to **Block** (not detect-only or log-only).

Save the policy. SafeLine now returns HTTP 403 for requests that match the managed rule set.

## Step 7: Trigger a blocked attack request

Send a simple SQL injection probe through the WAF:

```bash
curl -s -o /dev/null -w "%{http_code}\n" \
  "http://YOUR_FLOATING_IP/?id=1%27%20OR%20%271%27%3D%271"
```

The status code should be `403`. A clean request should still pass:

```bash
curl -s http://YOUR_FLOATING_IP/
```

The response body should still include `backend ok`.

## Step 8: Lock the backend security group to the WAF

Restrict the backend so it accepts application traffic only from the WAF security group `edge-waf-sg`. Remove any rule that opens the application port to `0.0.0.0/0` if you added one during testing.

```bash
openstack security group rule create \
  --protocol tcp \
  --dst-port 8080 \
  --remote-group edge-waf-sg \
  app-backend-sg
```

List the backend rules and confirm port 8080 allows only the WAF group:

```bash
openstack security group rule list app-backend-sg -f table
```

The backend has no floating IP, so it is not reachable directly from the internet. Only the WAF can forward traffic to it on the private subnet.

Verify clean traffic still passes through the WAF:

```bash
curl -s http://YOUR_FLOATING_IP/
```

The response should still show `backend ok`.

## Step 9: Confirm the origin cannot be reached directly

The backend never had a public address. After step 8, even a host on your project network cannot reach the application port unless its traffic originates from the WAF security group. If you temporarily attached a floating IP to the backend during testing, remove it before you treat the deployment as complete.

For TLS on the protected site, point your domain's DNS **A record** at `YOUR_FLOATING_IP` and enable HTTPS in the SafeLine dashboard for the site. Follow [How to point a domain at a Quake AI resource](/docs/network/how-to/point-domain-to-quake-ai). For certificate background, see [How to issue and auto-renew a TLS certificate with Let's Encrypt](/docs/network/how-to/lets-encrypt-certificate).

## What you built

- **Applied the `edge-waf` template** to provision a private network, WAF security group, data volume, SafeLine host, and floating IP
- **Launched a private backend** on the same subnet at `10.42.0.10` with no public address
- **Configured a protected site** in the SafeLine dashboard with managed rules in blocking mode
- **Confirmed attack traffic is blocked** with a 403 on an injection probe while clean traffic passes
- **Locked the backend security group** so application traffic accepts only the WAF as its source

## Scope of this deployment

This WAF runs in one region on a VM you operate. It inspects HTTP semantics for injection, cross-site scripting, and related L7 attack patterns. It is not a CDN and it does not absorb L3/L4 volumetric DDoS traffic. Quake AI has no anycast, no global PoPs, and no first-party CDN. For geographic distribution and edge absorption, [front the origin with a third-party CDN](/docs/network/how-to/front-with-cdn). For the broader WAF decision (CDN-layer vs self-managed), see [front with a WAF](/docs/network/how-to/front-with-waf).

The walkthrough adds a second small instance for the backend; that VM is not part of the OpenTofu template. Size the WAF with the template defaults; scale the backend independently for your application.

## Next steps

- [Edge WAF template](/resources/iac-templates/edge-waf): parameters, ports, and resource map
- [Front with a WAF](/docs/network/how-to/front-with-waf): CDN-layer vs self-managed patterns
- [Front with a CDN](/docs/network/how-to/front-with-cdn): cache static assets and absorb edge traffic geographically
- [Edge reverse proxy template](/resources/iac-templates/edge-reverse-proxy): TLS termination without a dedicated WAF engine
- [Security hardening checklist](/docs/security/hardening-checklist): audit security groups and floating IP usage

## Clean up

When you no longer need the deployment, destroy the OpenTofu stack and delete the backend resources you created in step 2:

```bash
tofu destroy
openstack server delete app-backend
openstack port delete app-backend-port
openstack security group delete app-backend-sg
```

Remove any DNS A record you pointed at the WAF floating IP.
