Edge web application firewall appliance
Edge web application firewall appliance
This pattern composes Compute, Network, and Block Storage into a self-hosted L7 web application firewall on infrastructure you control.
What this template does#
Provisions a single instance running SafeLine, an open-source WAF with a management dashboard, that inspects HTTP traffic on a floating IP and forwards clean requests to a private backend:
- Compute instance that runs SafeLine in Docker, sized for the detection engine (2 vCPU and 2 GiB RAM by default)
- Private network, subnet, router, port, and security group; a floating IP on the WAF only
- A block volume mounted at
/data, so rules, logs, and Postgres state live on a volume you can grow rather than on the boot disk - cloud-init installs Docker, downloads the official SafeLine compose bundle, generates a Postgres password on first boot, and starts SafeLine
The backend stays on the private subnet with no floating IP of its own. You configure the protected site in the SafeLine dashboard to forward to upstream_host and upstream_port, then lock the backend security group to accept traffic only from the WAF security group.
No credential ships with this template. Run docker exec safeline-mgt resetadmin on the instance after first boot to retrieve the one-time admin password.
Honest scope#
This WAF runs in one region on a VM you operate. It is an L7 application firewall: it inspects HTTP semantics for injection, cross-site scripting, and related attack patterns. It is not a CDN and it does not absorb L3/L4 volumetric DDoS traffic. For geographic distribution and edge absorption, front the origin with a third-party CDN or use a CDN-layer WAF.
Alternate engines#
This template leads with SafeLine (GPL-3.0, semantic-analysis engine, dashboard, Docker install). BunkerWeb fits when you want Nginx with OWASP CRS and security-by-default reverse-proxy rules. Coraza fits when you embed WAF logic inside Caddy or Envoy on the edge reverse proxy template instead of running a separate appliance.
Parameters#
| Parameter | Description | Default |
|---|---|---|
key_name | SSH keypair name (must already exist) | No default |
flavor_name | Instance size (SafeLine scales with CPU; throughput is bounded per core) | s1a.small |
image_name | Operating system image | Ubuntu-24.04 |
app_name | Display name prefix for resources | edge-waf |
safeline_version | SafeLine container image tag | latest |
mgt_port | Management dashboard port (reach via SSH tunnel) | 9443 |
domain | Public hostname for the protected site (used in waf_url output) | "" |
upstream_host | Private IP of the backend instance | 10.42.0.10 |
upstream_port | TCP port the backend listens on | 8080 |
volume_size | Block volume size in GiB, mounted at /data | 20 |
external_network | External network for floating IP allocation | PublicStatic |
private_cidr | CIDR for the private subnet | 10.42.0.0/24 |
Ports and access#
| Port | Purpose |
|---|---|
| 22 | Host SSH for administration |
| 80 | HTTP entry through the WAF after you configure the protected site |
| 443 | HTTPS entry when TLS is enabled on the protected site |
| 9443 | SafeLine management dashboard (not opened in the security group; use an SSH tunnel) |
When to use this pattern#
Run your own L7 WAF on a VM you operate so attack traffic is filtered before it reaches a private app backend. This security-focused template in the edge and gateway family implements the self-managed WAF path from front with a WAF.
For the TLS-only front door without a WAF engine, see the edge reverse proxy template. For DNS join points, see point a domain to Quake AI.
Estimated cost#
Monthly cost estimate
Pricing calculator ↗Sized as a custom package on shared vCPU.
Monthly total for the required template above. Use the configurator below to add optional pieces and see the total update.
What each resource is for
Waf
s1a.small · 2 shared vCPU, 2 GiB RAM, 0.5 Gbps
Compute shown per role at custom-package rates ($29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM). The headline above is the billed total: the cheaper of a named plan and the custom package, plus add-ons.
Included in baseline
s1a.small
2 shared vCPU, 2 GiB RAM, 0.5 Gbps
Compute + RAM rate basis
2 vCPU + 2 GiB RAM at $29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM (regular). Totals apply the flat −$5/mo package promotion.
Block storage (40 GiB)
40 GiB at $0.08/GiB/mo
Public IP (included)
1 included with the custom package
Package promotional discount
Flat −$5.00/mo on the custom package (same promotion as named plans).
Included at no charge
These line items are zero on Quake AI. Many other providers meter them separately.
Data transfer (inbound and outbound)
Unlimited data transfer on every plan; Quake AI does not meter per-GB egress.
AWS, GCP, and Azure meter outbound transfer per GB. DigitalOcean and Hetzner include an allowance on compute plans, then charge overage.
Learn morePrivate networking
Private networks, subnets, Neutron routers, and security groups are included with the plan.
VPC objects are usually free to create elsewhere, but NAT gateways bill hourly plus per-GB processed. Quake AI uses router SNAT with no separate NAT line item.
Control-plane API requests
OpenStack API calls for provisioning and management are included.
Some managed services on other clouds meter API calls or charge for premium control-plane features.
Pricing data last validated: . For current rates, check quake.ai/pricing.
Template source#
This is a validated OpenTofu template.
Show source (7 files)Hide source
data "openstack_images_image_v2" "os" {
name = var.image_name
most_recent = true
}
data "openstack_networking_network_v2" "external" {
name = var.external_network
}
resource "openstack_networking_network_v2" "private" {
name = "${var.app_name}-net"
admin_state_up = true
}
resource "openstack_networking_subnet_v2" "private" {
name = "${var.app_name}-subnet"
network_id = openstack_networking_network_v2.private.id
cidr = var.private_cidr
ip_version = 4
dns_nameservers = ["1.1.1.1", "8.8.8.8"]
}
resource "openstack_networking_router_v2" "main" {
name = "${var.app_name}-router"
external_network_id = data.openstack_networking_network_v2.external.id
}
resource "openstack_networking_router_interface_v2" "private" {
router_id = openstack_networking_router_v2.main.id
subnet_id = openstack_networking_subnet_v2.private.id
}
resource "openstack_networking_secgroup_v2" "waf" {
name = "${var.app_name}-sg"
description = "SSH and HTTP/HTTPS for the edge WAF appliance"
}
resource "openstack_networking_secgroup_rule_v2" "ssh" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 22
port_range_max = 22
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.waf.id
}
resource "openstack_networking_secgroup_rule_v2" "http" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 80
port_range_max = 80
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.waf.id
}
resource "openstack_networking_secgroup_rule_v2" "https" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 443
port_range_max = 443
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.waf.id
}
resource "openstack_networking_port_v2" "waf" {
name = "${var.app_name}-port"
network_id = openstack_networking_network_v2.private.id
security_group_ids = [openstack_networking_secgroup_v2.waf.id]
fixed_ip {
subnet_id = openstack_networking_subnet_v2.private.id
}
depends_on = [openstack_networking_router_interface_v2.private]
}
resource "openstack_blockstorage_volume_v3" "data" {
name = "${var.app_name}-data"
size = var.volume_size
}
resource "openstack_compute_instance_v2" "waf" {
name = var.app_name
flavor_name = var.flavor_name
key_pair = var.key_name
user_data = templatefile("${path.module}/cloud-init/waf.yaml.tftpl", {
safeline_version = var.safeline_version
mgt_port = var.mgt_port
upstream_host = var.upstream_host
upstream_port = var.upstream_port
})
block_device {
uuid = data.openstack_images_image_v2.os.id
source_type = "image"
destination_type = "volume"
volume_size = 20
boot_index = 0
delete_on_termination = true
}
network {
port = openstack_networking_port_v2.waf.id
}
}
resource "openstack_compute_volume_attach_v2" "data" {
instance_id = openstack_compute_instance_v2.waf.id
volume_id = openstack_blockstorage_volume_v3.data.id
}
resource "openstack_networking_floatingip_v2" "waf" {
pool = var.external_network
}
resource "openstack_networking_floatingip_associate_v2" "waf" {
floating_ip = openstack_networking_floatingip_v2.waf.address
port_id = openstack_networking_port_v2.waf.id
}
variable "key_name" {
description = "SSH keypair name (must already exist in your project)"
type = string
}
variable "flavor_name" {
description = "Instance size. SafeLine's detection engine scales with CPU: 2 vCPU and 2 GiB RAM handle a modest app backend. Raise the flavor for higher request rates; throughput is bounded by per-core inspection capacity, not connection count alone."
type = string
default = "s1a.small"
}
variable "image_name" {
description = "Operating system image. cloud-init targets a Debian-family distribution; Ubuntu 24.04 is the recommended base."
type = string
default = "Ubuntu-24.04"
}
variable "app_name" {
description = "Display name prefix for compute and network resources"
type = string
default = "edge-waf"
}
variable "safeline_version" {
description = "SafeLine container image tag from the Chaitin release channel. The default latest tracks the current community edition; pin a specific tag for reproducible rebuilds."
type = string
default = "latest"
}
variable "mgt_port" {
description = "TCP port for the SafeLine management dashboard on the instance. The security group opens only 22, 80, and 443; reach the dashboard through an SSH tunnel to this port."
type = number
default = 9443
}
variable "domain" {
description = "Public hostname you plan to protect with the WAF. When set, waf_url in the outputs uses https:// on this domain after you point its DNS A record at the floating IP and configure the site in the SafeLine dashboard. Leave empty to test over HTTP on the floating IP first."
type = string
default = ""
}
variable "upstream_host" {
description = "Private IP address of the backend instance the WAF forwards to after you configure the protected site. The backend stays on the private subnet with no floating IP; lock its security group to accept traffic only from this WAF instance's security group."
type = string
default = "10.42.0.10"
}
variable "upstream_port" {
description = "TCP port the backend listens on within the private network"
type = number
default = 8080
}
variable "volume_size" {
description = "Block volume size in GiB for SafeLine rules, logs, and Postgres state. The volume mounts at /data so WAF data lives on a resizable volume rather than the boot disk."
type = number
default = 20
}
variable "external_network" {
description = "Shared external network for router gateway and floating IPs; defaults to PublicStatic (persisted FIP / production pattern). Override with PublicEphemeral for ephemeral demos."
type = string
default = "PublicStatic"
}
variable "private_cidr" {
description = "CIDR for the private tenant network the WAF lives in"
type = string
default = "10.42.0.0/24"
}
output "instance_id" {
description = "ID of the compute instance running the WAF appliance"
value = openstack_compute_instance_v2.waf.id
}
output "floating_ip" {
description = "Public floating IP address of the WAF appliance"
value = openstack_networking_floatingip_v2.waf.address
}
output "private_ip" {
description = "Private IP address of the WAF on the tenant network"
value = openstack_compute_instance_v2.waf.access_ip_v4
}
output "waf_url" {
description = "URL for the WAF front door after you configure the protected site in the dashboard. HTTPS on the domain when set, otherwise HTTP on the floating IP."
value = var.domain != "" ? "https://${var.domain}" : "http://${openstack_networking_floatingip_v2.waf.address}"
}
output "dashboard_hint" {
description = "How to retrieve the SafeLine admin password and open the management dashboard"
value = "SSH to the instance, run: docker exec safeline-mgt resetadmin. Open the dashboard with an SSH tunnel: ssh -L ${var.mgt_port}:127.0.0.1:${var.mgt_port} ubuntu@${openstack_networking_floatingip_v2.waf.address}, then browse https://127.0.0.1:${var.mgt_port}/. Configure the protected site to forward to ${var.upstream_host}:${var.upstream_port}."
}
terraform {
required_version = ">= 1.6.0"
required_providers {
openstack = {
source = "terraform-provider-openstack/openstack"
version = "~> 2.0"
}
}
}
provider "openstack" {}
# Required: SSH keypair must already exist in your project
key_name = "YOUR_KEY_NAME"
# Backend on the private subnet (no floating IP). After apply, configure the
# protected site in the SafeLine dashboard to forward to this address, then lock
# the backend security group to the WAF security group.
# upstream_host = "10.42.0.10"
# upstream_port = 8080
# Recommended: set a domain and point its DNS A record at the floating IP from
# the outputs after you configure TLS in the SafeLine dashboard.
# domain = "app.example.com"
# safeline_version = "latest"
# mgt_port = 9443
# flavor_name = "s1a.small"
# image_name = "Ubuntu-24.04"
# app_name = "edge-waf"
# volume_size = 20
# external_network = "PublicStatic"
# private_cidr = "10.42.0.0/24"
#cloud-config
package_update: true
packages:
- ca-certificates
- curl
- wget
runcmd:
- |
set -e
# The data volume attaches as /dev/sdb on this platform. Mount it at /data
# before SafeLine starts so rules, logs, and Postgres state live on the
# resizable volume rather than the boot disk.
DEV=/dev/sdb
for i in $(seq 1 30); do [ -b "$DEV" ] && break; sleep 5; done
if ! blkid "$DEV" >/dev/null 2>&1; then mkfs.ext4 -F -L wafdata "$DEV"; fi
mkdir -p /data/safeline
mount "$DEV" /data
grep -q "$DEV" /etc/fstab || echo "$DEV /data ext4 defaults,nofail 0 2" >> /etc/fstab
curl -fsSL https://get.docker.com | sh
cd /data/safeline
wget -q "https://waf.chaitin.com/release/latest/compose.yaml" -O compose.yaml
POSTGRES_PASSWORD="$(openssl rand -hex 16)"
cat > /data/safeline/.env <<EOF
SAFELINE_DIR=/data/safeline
IMAGE_TAG=${safeline_version}
MGT_PORT=${mgt_port}
POSTGRES_PASSWORD=$POSTGRES_PASSWORD
SUBNET_PREFIX=172.22.222
IMAGE_PREFIX=chaitin
ARCH_SUFFIX=
RELEASE=
REGION=-g
MGT_PROXY=0
EOF
docker compose up -d
# Upstream ${upstream_host}:${upstream_port} is configured in the SafeLine dashboard
# after you run: docker exec safeline-mgt resetadmin
# Edge WAF appliance
Single compute instance running [SafeLine](https://github.com/chaitin/SafeLine), an open-source L7 web application firewall, on infrastructure you control. The WAF holds the public floating IP, inspects HTTP traffic, and forwards clean requests to a private backend that has no public address of its own.
**Network class:** production — `external_network` defaults to `PublicStatic` for persisted floating IPs and multi-tier stacks; override with `PublicEphemeral` for ephemeral demos.
The instance provisions a private network, a floating IP, and a block volume mounted at `/data` so SafeLine rules, logs, and Postgres state live on a resizable volume rather than the boot disk. cloud-init installs Docker, downloads the official SafeLine compose bundle, generates a Postgres password on first boot, and starts SafeLine.
## Where this fits
This template productizes Option B from [front with a WAF](/docs/network/how-to/front-with-waf): a self-managed WAF appliance on a VM you operate. It shares the same single-VM edge shape as the [edge reverse proxy](/resources/iac-templates/edge-reverse-proxy) template but runs a dedicated WAF engine with a management dashboard instead of plain TLS termination.
This WAF runs in one region on a VM you operate. It is an L7 application firewall, not a CDN and not L3/L4 volumetric DDoS scrubbing. For geographic distribution and volumetric absorption, [front the origin with a third-party CDN](/docs/network/how-to/front-with-cdn) or a [CDN-layer WAF](/docs/network/how-to/front-with-waf).
## Prerequisites
- OpenTofu >= 1.6.0 or Terraform >= 1.6.0
- Quake AI account with OpenStack credentials
- An existing SSH keypair in your project (the value of `key_name` must match that keypair)
- A backend instance on the private network (or plan to deploy one after the WAF)
## Resource baseline
SafeLine's detection engine scales with CPU. The default `s1a.small` flavor (2 shared vCPU, 2 GiB RAM) handles a modest app backend. Raise the flavor when request rates or body sizes push per-core inspection limits. The boot disk is 20 GiB; WAF state lives on the separate data volume (`volume_size`, default 20 GiB).
## Usage
1. Clone or copy this template directory
2. Copy `terraform.tfvars.example` to `terraform.tfvars` and set `key_name`, `upstream_host`, and `upstream_port` (and `domain` if you have one)
3. Source your OpenStack credentials: `source openrc.sh`
4. Initialize: `tofu init`
5. Preview: `tofu plan`
6. Apply: `tofu apply`
cloud-init takes several minutes on first boot to install Docker, mount the data volume, and start SafeLine. After boot, retrieve the admin password and configure the protected site (see Dashboard and upstream below).
## Dashboard and upstream
SafeLine does not ship admin credentials in this repository. After apply:
1. SSH to the instance at `floating_ip`
2. Run `docker exec safeline-mgt resetadmin` and record the one-time admin username and password
3. Open the dashboard through an SSH tunnel: `ssh -L 9443:127.0.0.1:9443 ubuntu@FLOATING_IP`, then browse `https://127.0.0.1:9443/`
4. Add a protected site that forwards to `upstream_host`:`upstream_port` on the private subnet
5. Point your domain's DNS A record at `floating_ip` when you enable TLS on the site
Lock the backend security group so it accepts traffic on `upstream_port` only from the WAF security group (`edge-waf-sg` by default).
## Alternate engines
This template leads with SafeLine (GPL-3.0, semantic-analysis engine, management dashboard). [BunkerWeb](https://www.bunkerweb.io) fits when you want Nginx with OWASP CRS and security-by-default reverse-proxy rules. [Coraza](https://coraza.io) fits when you embed WAF logic inside Caddy or Envoy on the [edge reverse proxy](/resources/iac-templates/edge-reverse-proxy) template instead of running a separate appliance. Swap the container stack in cloud-init if you prefer one of them.
## Outputs
| Output | Description |
| --- | --- |
| `floating_ip` | Public floating IP of the WAF appliance |
| `private_ip` | Private IP of the WAF on the tenant network |
| `waf_url` | HTTPS on the domain when set, otherwise HTTP on the floating IP |
| `dashboard_hint` | Commands to retrieve admin credentials and open the dashboard |
| `instance_id` | Compute instance ID |
## Validation
This template passes `tofu validate` in CI. That check confirms the OpenTofu configuration is well-formed against the provider schema; it does not run `tofu apply` against a live account.
Resources, parameters, and variables
key_namerequiredflavor_name="s1a.small"image_name="Ubuntu-24.04"app_name="edge-waf"safeline_version="latest"mgt_port=9443domain=""upstream_host="10.42.0.10"upstream_port=8080volume_size=20external_network="PublicStatic"private_cidr="10.42.0.0/24"
Customize this pattern#
- Customize a template's image and flavor
- Add a block volume to a template
- Parameterize a template with a tfvars file
See also#
Usage Guidelines
The sample code, software libraries, command line tools, proofs of concept, templates, and other related technology on this page (including any of the foregoing that is provided by Quake AI personnel) is provided to you as Quake AI Content under the Quake AI Customer Agreement, or the relevant written agreement between you and Quake AI (whichever applies). Do not use this Quake AI Content in your production accounts, or on production or other critical data. You are responsible for testing, securing, and optimizing the Quake AI Content (such as sample code) as appropriate for production grade use based on your specific quality control practices and standards. Deploying Quake AI Content may incur Quake AI charges for creating or using Quake AI chargeable resources, such as running Compute instances or storing data in Object Storage. Your use is also subject to the Acceptable Use Policy.
For the full policy, see Usage Guidelines.
Last validated: 07.07.2026
See Also
Terraform and OpenTofu on Quake AI
Prerequisite
Networks
Prerequisite
Authoring IaC templates for Quake AI
Shares: Volumes, Security Groups
Deploy an API gateway with the api-gateway template
Shares: Volumes, Security Groups
Deploy a regional edge cache with the edge-cache template
Shares: Volumes, Security Groups