Skip to content

Latest commit

 

History

463 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

PRX-WAF

High-performance WAF built on Pingora

CI Sec Audit License Rust PostgreSQL

PRX-WAF is a production-ready Web Application Firewall proxy built on Pingora (Cloudflare's Rust HTTP proxy library). It combines multi-phase attack detection, a Rhai scripting engine, ModSecurity rule support, CrowdSec integration, WASM plugins, and a Vue 3 admin UI into a single deployable binary.


Features

  • Pingora reverse proxy — HTTP/1.1, HTTP/2, HTTP/3 via QUIC (Quinn); weighted round-robin load balancing
  • 10+ attack detection checkers — SQL injection, XSS, RFI/LFI, SSRF, path traversal, command injection, scanner detection, protocol violations
  • libinjection-based SQLi/XSS detection — battle-tested libinjection fingerprint engine for accurate SQL injection and XSS detection with low false-positive rates
  • SSRF protection — URL validation with public-IP enforcement and scheme-allowlist modes; blocks requests to RFC-1918 / loopback / link-local addresses
  • DNS rebinding guard — IP pinning after initial DNS resolution prevents mid-request DNS rebinding attacks
  • Iterative URL decoding — up to 5 rounds of percent-decoding before analysis, preventing double/triple-encoding bypass techniques
  • Remote rule source loading — async fetch of rule sources with configurable size limits and timeouts; fails safe on unreachable sources
  • CC/DDoS protection — sliding-window rate limiting per IP with configurable thresholds
  • Rhai scripting engine — write custom detection rules in a sandboxed scripting language
  • OWASP CRS rule support — load and manage OWASP Core Rule Set in YAML format
  • ModSecurity rule parser — import SecRule directives (basic subset: ARGS, REQUEST_HEADERS, REQUEST_URI, REQUEST_BODY)
  • Override reload without downtimePOST /api/rules/reload rebuilds the per-rule override layer in the running process; the CRS rule files themselves are compiled once at startup and need a restart
  • Sensitive word detection — Aho-Corasick multi-pattern matching for PII / credential leakage
  • Anti-hotlinking protection — Referer-based validation per host
  • CrowdSec integration — Bouncer (decision cache from LAPI) + AppSec (remote HTTP inspection) + Log Pusher
  • WASM plugin system — sandboxed wasmtime runtime for custom logic
  • SSL/TLS automation — Let's Encrypt via instant-acme (ACME v2); auto-renewal
  • Tunnel / Zero-Trust access — WebSocket-based reverse tunnel (Cloudflare Tunnel-style)
  • Response caching — moka LRU in-memory cache with TTL and size limits
  • PostgreSQL 16+ storage — all configuration, rules, logs, and stats persisted
  • Vue 3 Admin UI — JWT + TOTP authentication; real-time WebSocket monitoring; embedded in the binary
  • Real-time WebSocket monitoring — live traffic stats and security event stream
  • Notification system — Email (SMTP), Webhook, Telegram alerts
  • AES-256-GCM encryption at rest — sensitive config values (API keys, passwords) encrypted in PostgreSQL
  • Docker & systemd deployment — signed multi-arch images on ghcr.io/openprx/prx-waf, Docker Compose files and systemd unit examples included

Quick Start

Docker Compose

Every tagged release publishes a multi-arch (linux/amd64 + linux/arm64) image to ghcr.io/openprx/prx-waf, so nothing is compiled here.

git clone https://github.com/openprx/prx-waf
cd prx-waf

# Configure secrets in one place: copy the template and fill it in.
cp .env.example .env
# At minimum set JWT_SECRET and MASTER_KEY (each >= 32 chars). Generate with:
#   openssl rand -hex 32
# docker-compose refuses to start if these required secrets are missing.

docker compose up -d          # pulls ghcr.io/openprx/prx-waf:latest

# Admin UI: http://localhost:16827  (docker-compose.yml maps host 16827 -> container 9527)
# Default credentials: admin / <ADMIN_PASSWORD, or the random one printed to the
# logs on first start>  (change immediately)

The clone is for the compose file, configs/ and rules/, which the compose file bind-mounts; the WAF itself comes from the registry.

The compose file defines the container's health check itself. The image does not carry one — a registry stores it as an OCI image, and the OCI image configuration has no field a HEALTHCHECK could go in. If you run the image with a bare docker run, pass --health-cmd 'curl -fsS http://localhost:9527/health' to get the same behaviour.

latest tracks the newest stable release and never a pre-release. Pin an exact version in production:

PRX_WAF_VERSION=v0.2.60 docker compose up -d

The image is signed with cosign in keyless mode — there is no long-lived signing key — and carries SLSA build provenance. Verify before you run it:

cosign verify ghcr.io/openprx/prx-waf:v0.2.60 \
  --certificate-identity-regexp '^https://github\.com/openprx/prx-waf/\.github/workflows/release\.yml@refs/tags/v.*$' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com'

gh attestation verify oci://ghcr.io/openprx/prx-waf:v0.2.60 --repo openprx/prx-waf

To run your own build instead of the published image:

cargo build --release
mkdir -p data      # Dockerfile.prebuilt copies data/; `prx-waf geoip download` fills it
docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build

All security-critical settings can be configured via .env / environment variables — see .env.example for the full, documented list (required secrets, reverse-proxy trust, cluster, database). Environment values override the matching TOML fields.

Manual Build

Prerequisites: Rust 1.94+, PostgreSQL 16+

The Rust floor is rust-version in the workspace manifest, so cargo names it before it compiles anything rather than failing somewhere in the dependency graph. Building the admin UI as well needs Node 20+ and npm ci && npm run build in web/admin-ui; crates/waf-api embeds web/admin-ui/dist into the binary at compile time, and a checkout ships that directory empty.

# Clone
git clone https://github.com/openprx/prx-waf
cd prx-waf

# Build release binary
cargo build --release

# Create database
createdb prx_waf
createuser prx_waf

# Run migrations and seed admin user
./target/release/prx-waf -c configs/default.toml migrate
./target/release/prx-waf -c configs/default.toml seed-admin

# Start the proxy + API server
./target/release/prx-waf -c configs/default.toml run

CLI Reference

prx-waf [OPTIONS] <COMMAND>

Options:
  -c, --config <FILE>   Config file path [default: configs/default.toml]

Commands:
  run          Start proxy + management API (blocks forever)
  run --upgrade  Take the listening sockets over from a prx-waf already running
                 on this host, so a config or binary change costs no dropped
                 connections. See docs/graceful-upgrade.md for the procedure —
                 the order of the two commands matters.
  migrate      Run database migrations only
  seed-admin   Create default admin user (username: admin, password: $ADMIN_PASSWORD if set, else a random 24-char password printed once to stdout)
  crowdsec     CrowdSec integration management
  rules        Rule management (list, load, validate, hot-reload)
  sources      Rule source management (add, remove, sync)
  bot          Bot detection management (list, add, test)

CrowdSec Commands

prx-waf crowdsec status             # Show integration status
prx-waf crowdsec decisions          # List active decisions from LAPI
prx-waf crowdsec test               # Test LAPI connectivity
prx-waf crowdsec setup              # Interactive setup wizard

Rule Management Commands

Reads report the rule set this build actually enforces (rules/owasp-crs/, with the operator overrides from the database layered on), so they answer "what is this WAF doing" rather than "what is in some file". Run them from the deployment root — the CRS path is relative to the working directory.

# What is enforced, and in what state
prx-waf rules list                        # Every enforced rule + its effective state
prx-waf rules list --category sqli        # Filter by category
prx-waf rules list --source sqli          # Filter by source file
prx-waf rules list --state disabled       # active | disabled | log_only | overridden
prx-waf rules list --host <code>          # States in force for one host
prx-waf rules info <rule-id>              # One rule in detail (CRS-942100 or 942100)
prx-waf rules search <query>              # Same listing, filtered by id/name/category/source
prx-waf rules stats                       # Enforced/declared/rejected + override totals

# Changing a rule (writes an override row; see "Reloading" below)
prx-waf rules enable <rule-id>            # Cancel an override
prx-waf rules disable <rule-id>           # Stop evaluating the rule
prx-waf rules disable <rule-id> --log-only  # Keep evaluating + auditing, stop scoring

# Rule files
prx-waf rules validate <path>             # Parse-check a rule file (reads `[rules]`, not the enforced set)
prx-waf rules export [--format yaml] [--host CODE]
                                          # Dump the enforced set — same source as `list`/`stats`.
                                          # Inventory on stdout, everything else on stderr.
prx-waf rules import <path|url>           # Not implemented; exits non-zero
prx-waf rules update                      # Withheld pending supply-chain review; exits non-zero

# Source management
prx-waf sources list                      # Print the configured `[[rules.sources]]`
# add / remove / update / sync are not implemented: they exit non-zero and tell
# you to edit the config file instead of pretending to have a writable store.

# Bot detection
prx-waf bot list                          # List known bot signatures
prx-waf bot add <pattern> [--action block|captcha|log]
prx-waf bot remove <pattern>
prx-waf bot test <user-agent>             # Test a user-agent against bot rules

Configuration

Configuration is loaded from a TOML file (default: configs/default.toml).

[proxy]
listen_addr     = "0.0.0.0:80"
# Bound only once a certificate resolves — from tls_cert_pem/tls_key_pem if set,
# otherwise from the `certificates` table that ACME issues into. With neither,
# the port is left unbound and startup says so. One certificate serves the whole
# port (no SNI), and it is read at startup, so a renewal takes effect on the
# next start; `prx-waf run --upgrade` does that without dropping connections.
listen_addr_tls = "0.0.0.0:443"
# tls_cert_pem  = "/etc/prx-waf/tls/cert.pem"   # both, or neither
# tls_key_pem   = "/etc/prx-waf/tls/key.pem"
worker_threads  = 4          # optional; unset or 0 = the CPUs this process may use

[api]
listen_addr = "127.0.0.1:9527"

[storage]
database_url    = "postgresql://prx_waf:prx_waf@127.0.0.1:5432/prx_waf"
max_connections = 20

[cache]
enabled          = true
max_size_mb      = 256
default_ttl_secs = 60
max_ttl_secs     = 3600

[http3]
enabled     = false
listen_addr = "0.0.0.0:443"
cert_pem    = "/etc/ssl/certs/server.pem"
key_pem     = "/etc/ssl/private/server.key"
# On a non-443 listen port, every [[hosts]] entry served over HTTP/3 must
# declare that same port — see "HTTP/3 on a non-default port" below.

[security]
admin_ip_allowlist      = []        # empty = allow all
max_request_body_bytes  = 10485760  # 10 MB
api_rate_limit_rps      = 100
cors_origins            = []

# --- Rule Management ---
[rules]
dir                    = "rules/"   # rules directory to watch
hot_reload             = true       # enable file watching
reload_debounce_ms     = 500
enable_builtin_owasp   = true       # built-in OWASP CRS subset
enable_builtin_bot     = true       # built-in bot detection
enable_builtin_scanner = true       # built-in scanner detection

# Remote rule sources
[[rules.sources]]
name   = "custom"
path   = "rules/custom/"
format = "yaml"

[[rules.sources]]
name            = "owasp-crs"
url             = "https://example.com/rules/owasp.yaml"
format          = "yaml"
update_interval = 86400  # 24h in seconds

# --- Cluster ---
[cluster]
enabled     = false
node_id     = ""                  # auto-generated if empty
role        = "auto"              # auto | main | worker
listen_addr = "0.0.0.0:16851"    # QUIC inter-node communication
seeds       = []                  # seed node addresses

[cluster.crypto]
auto_generate = true

# --- CrowdSec Integration ---
[crowdsec]
enabled               = false
mode                  = "bouncer"   # bouncer | appsec | both
lapi_url              = "http://127.0.0.1:8080"
api_key               = ""
update_frequency_secs = 10
fallback_action       = "allow"     # allow | block | log — what the bouncer does
                                    # when LAPI is unreachable AND the decision
                                    # cache is empty (a stale-but-populated cache
                                    # keeps enforcing and does not trigger this).
                                    # "block" refuses every request during a LAPI
                                    # outage; keep "allow" unless you mean it.

# Optional: AppSec endpoint
# appsec_endpoint = "http://127.0.0.1:7422"
# appsec_key      = "<appsec-key>"

# --- Static hosts (also managed via Admin UI / DB) ---
# [[hosts]]
# host        = "example.com"
# port        = 80
# remote_host = "127.0.0.1"
# remote_port = 8080
# ssl         = false
# guard_status = true
# upstream_ssl = false   # see "Site TLS vs upstream TLS" below

Site TLS vs upstream TLS

ssl describes the site: it is the flag ACME issues and renews a certificate against. It is not a statement about the origin, but until upstream_ssl existed it decided that too — on HTTP/1.1 and HTTP/3 alike — so the commonest reverse-proxy arrangement there is,

[[hosts]]
host        = "site.example"
port        = 443
ssl         = true          # public HTTPS
remote_host = "127.0.0.1"
remote_port = 8080          # plaintext origin

dialled https://127.0.0.1:8080 and answered 502 Bad Gateway. Add upstream_ssl = false and the origin is dialled in cleartext while the site keeps its certificate; upstream_ssl = true on a plaintext site does the reverse.

Leaving upstream_ssl unset keeps the old behaviour exactly — it falls back to ssl — so nothing changes on upgrade, and in particular an origin connection that really was encrypted is not silently downgraded. Every host still relying on that fallback is named in a warning at startup; setting upstream_ssl either way silences it.

HTTP/3 on a non-default port

A [[hosts]] entry is routable under host:port. It is additionally routable under the bare host only when its port is 80 or 443 — a request for example.com:31337 does not inherit the example.com:80 policy, deliberately.

HTTP/3 carries no Host header. It is routed on RFC 9114's :authority, and clients include the port there whenever it is not the scheme default: curl --http3 https://site.example:18443/ sends :authority: site.example:18443. So an off-port HTTP/3 listener must be matched by hosts declared on that same port:

[http3]
enabled     = true
listen_addr = "0.0.0.0:18443"

[[hosts]]
host        = "site.example"
port        = 18443          # the [http3] listen port — not 443
remote_host = "127.0.0.1"
remote_port = 8080

Declare port = 443 instead and every HTTP/3 request 404s: the lookup key is site.example:18443, the registered key is site.example:443, and the bare-hostname fallback is withheld because 18443 is not a default port. The upstream is never contacted, and nothing in the log mentions the port.

The rule is the router's, not HTTP/3's — HTTP/1.1 on a non-default port has always behaved this way. HTTP/3 is just the first protocol here that is commonly run off-port, e.g. unprivileged or behind a NAT. Serving one site on both a default and a non-default port takes one [[hosts]] entry per port.


Rule Management

PRX-WAF supports multiple rule formats and sources. Rules are loaded at startup and can be hot-reloaded without downtime.

Rule Formats

Format Extension Description
YAML .yaml, .yml Native PRX-WAF format
ModSecurity .conf SecRule directives (basic subset)
JSON .json JSON array of rule objects

YAML Rule Format

- id: "CUSTOM-001"
  name: "Block admin path"
  description: "Block access to /admin from untrusted IPs"
  category: "access-control"
  source: "custom"
  enabled: true
  action: "block"
  severity: "high"
  pattern: "^/admin"
  tags:
    - "admin"
    - "access-control"

ModSecurity Rule Format (basic subset)

SecRule REQUEST_URI "@rx /admin" \
    "id:1001,phase:1,deny,status:403,msg:'Admin path blocked'"

SecRule ARGS "@contains <script>" \
    "id:1002,phase:2,deny,status:403,msg:'XSS attempt'"

Reloading

Two different things can change, and they reload differently.

Operator overrides (rules enable / rules disable, the Admin UI, or a direct edit of the rule_overrides table) live in the database. A running process rebuilds that layer in place, with no dropped connections:

curl -X POST http://127.0.0.1:9527/api/rules/reload \
     -H "Authorization: Bearer <admin JWT>"

The Admin UI and POST /api/rules/overrides already trigger this on every write, so the endpoint is only needed for changes made out of band — the CLI against a shared database, a migration, a psql session.

The CRS rule files under rules/owasp-crs/ are parsed and compiled into matchers once, at startup. They are not re-read while the process is serving: restart to pick up an edited rule file. There is no file watcher and no SIGHUP handler — prx-waf rules reload is not implemented and says so.

Built-in Rules

Built-in rules are compiled into the binary and loaded automatically (configurable):

  • OWASP CRS — Common attack signatures (SQLi, XSS, RCE, scanner detection)
  • Bot Detection — Known malicious bots, AI crawlers, headless browsers
  • Scanner Detection — Vulnerability scanner fingerprints (Nmap, Nikto, etc.)

Architecture

PRX-WAF is organized as a 7-crate Cargo workspace:

prx-waf/
├── crates/
│   ├── prx-waf/        Binary: CLI entry point, server bootstrap
│   ├── gateway/        Pingora proxy, HTTP/3, SSL automation, caching, tunnels
│   ├── waf-engine/     Detection pipeline, rules engine, checks, plugins, CrowdSec
│   ├── waf-storage/    PostgreSQL layer (sqlx), migrations, models
│   ├── waf-api/        Axum REST API, JWT/TOTP auth, WebSocket, static UI
│   ├── waf-common/     Shared types: RequestCtx, WafDecision, HostConfig, config
│   └── waf-cluster/    Cluster consensus, QUIC transport, rule sync, certificates
├── migrations/         SQL migration files (0001–0014)
├── configs/            Example TOML config files
├── rules/              Rule files directory (YAML, ModSec, JSON)
└── web/admin-ui/       Vue 3 admin SPA (served embedded in waf-api)

Request Flow

Client Request
    │
    ▼
Pingora Listener (TCP/TLS/QUIC)
    │
    ▼
WafEngine Pipeline (18 phases)
    ├── Phase 1-4:  IP/URL whitelist + blacklist (CIDR)
    ├── Phase 5:    CC/DDoS rate limiting
    ├── Phase 6:    Scanner detection
    ├── Phase 7:    Bot detection
    ├── Phase 8:    SQL injection
    ├── Phase 9:    XSS
    ├── Phase 10:   RCE / command injection
    ├── Phase 11:   Directory traversal
    ├── Phase 12:   Custom rules (Rhai scripts + JSON DSL)
    ├── Phase 13:   OWASP CRS
    ├── Phase 14:   Sensitive data detection
    ├── Phase 15:   Anti-hotlinking
    ├── Phase 16:   CrowdSec bouncer + AppSec
    ├── Phase 17:   GeoIP access control
    └── Phase 18:   Community threat-intelligence blocklist
    │
    ▼
HostRouter → Upstream (with load balancing)

Performance

Measured with tests/perf/run.sh; full numbers, conditions and caveats in tests/perf/RESULTS.md.

On an AMD Ryzen 9 5900HX (8 physical cores, 16 logical), shipping default configuration, plaintext HTTP/1.1, oha at 50 connections against a near-zero-work origin, with the WAF pinned to four hardware threads on two physical cores:

workload origin prx-waf, no detection prx-waf, shipping default added latency (p50, unsaturated)
small GET 169,700 rps 58,078 rps 9,517 rps +0.42 ms
1 KiB JSON POST 160,776 rps 45,832 rps 1,789 rps +2.18 ms
64 KiB multipart upload 89,098 rps 21,764 rps 4,237 rps +0.90 ms
1 MiB body POST 26,889 rps 2,130 rps 306 rps not measured

Three things an operator should know before deploying:

  • These are four-worker-thread numbers on two physical cores. [proxy] worker_threads sizes the data plane and, unset, follows the CPUs the process may actually use — the cgroup quota and affinity mask, not nproc. Releases before it ran the whole data plane on one thread whatever the machine, and every figure recorded then was roughly 2–3× lower.
  • Cost is driven by request-body size, not request rate — and the shipped defaults now bound it. [content_security.lane1] max_body_bytes defaults to 64 KiB, the same boundary CRS already draws, which is worth 20–75× on large-body workloads: a 64 KiB upload costs 565 µs of CPU under Lane 1 against 42.7 ms unbounded. The price is that sqli / xss / rce / dir_traversal see none of a body over 64 KiB — not a truncated prefix, none of it. Raise the key if your application legitimately posts larger bodies.
  • Sustained attack traffic grows memory to gigabytes (106 MiB → 4.1 GiB in ten seconds), because every blocked request writes to the database through a 20-connection pool. This got worse, not better, when the data plane went wide. Give the process a memory limit.

Resource limits, queue capacities, timeouts and what happens when each is exceeded are documented separately in docs/dos-budget.md, and every one of them now has a counter on /metrics — see docs/metrics.md.

This is deliberately not a CI gate — see tests/perf/README.md.


Metrics

Prometheus exposition on 127.0.0.1:9127/metrics, on by default. (Not 9090: that is Prometheus's own port, and a node-local Prometheus is the intended scraper.)

curl -s http://127.0.0.1:9127/metrics
[metrics]
enabled = true
listen_addr = "127.0.0.1:9127"
max_host_labels = 128

Seven metrics: RED by host and decision, the detection mix by phase, per-lane detection cost (Lane 1 / CRS / Lane 2 are timed separately, because they share no work and their cost ordering inverts with body size), and a counter for every bounded resource in docs/dos-budget.md — so "how much inspection am I losing" is a query rather than a log grep.

Cardinality is a property of the config, not of the traffic. Every label except host is a compile-time enumeration; host is bounded by max_host_labels and folds into __other__ past it. Client IP, rule id, path and user agent are never labels. Total series is 28 × max_host_labels + 214 — about 3 800 at the default, whatever the traffic does.

The endpoint is unauthenticated by design and its own listener, not a route on the management API: Prometheus cannot carry a JWT well, and reusing the admin token would put a credential that can rewrite rules and replace certificates into the monitoring system's config file. The bind address is the access control, which is why it defaults to loopback and warns at startup when it does not. Full reference: docs/metrics.md.


API Reference

The management API listens on 0.0.0.0:9527 by default (as shipped in configs/default.toml; the code-level struct default is 127.0.0.1:9527, but every documented run path here loads configs/default.toml, which overrides it — see [security] admin_ip_allowlist to restrict access). All endpoints (except /api/auth/login) require a JWT Bearer token.

Authentication

POST /api/auth/login
Content-Type: application/json

{"username": "admin", "password": "<admin-password>", "totp_code": "123456"}

→ {"token": "eyJ...", "refresh_token": "..."}

Key Endpoints

Method Path Description
GET /health Health check (public)
POST /api/auth/login Obtain JWT token
POST /api/auth/logout Invalidate session
POST /api/auth/refresh Refresh JWT token
GET/POST /api/hosts List / add proxy hosts
GET/PUT/DELETE /api/hosts/:id Get / update / delete host
GET/POST /api/allow-ips List / add IP allowlist entries
DELETE /api/allow-ips/:id Remove IP allowlist entry
GET/POST /api/block-ips List / add IP blocklist entries
DELETE /api/block-ips/:id Remove IP blocklist entry
GET/POST /api/allow-urls List / add URL allowlist entries
DELETE /api/allow-urls/:id Remove URL allowlist entry
GET/POST /api/block-urls List / add URL blocklist entries
DELETE /api/block-urls/:id Remove URL blocklist entry
GET /api/attack-logs Attack log entries
GET /api/security-events Security event stream history
GET /api/status System status
POST /api/reload Hot-reload rules
GET/POST /api/custom-rules List / create custom rules
DELETE /api/custom-rules/:id Delete custom rule
GET/POST /api/sensitive-patterns List / add sensitive word patterns
DELETE /api/sensitive-patterns/:id Delete sensitive pattern
GET/POST /api/bot-patterns Built-in bot catalogue + operator patterns / add one (admin-only, reads included)
PUT/DELETE /api/bot-patterns/:id Update / delete an operator bot pattern
GET /api/bot-patterns/test Evaluate a user_agent against the live rule set
GET/POST /api/hotlink-config Get / set anti-hotlink config
GET/POST /api/lb-backends List / add load-balancer backends
DELETE /api/lb-backends/:id Delete LB backend
GET/POST /api/certificates List / upload TLS certificates
DELETE /api/certificates/:id Delete certificate
GET /api/stats/overview Aggregated traffic statistics
GET /api/stats/timeseries Time-series traffic data
GET /api/stats/geo Geo-distribution statistics
GET/POST /api/notifications List / create notification channels
DELETE /api/notifications/:id Delete notification channel
GET /api/notifications/log Notification delivery log
POST /api/notifications/:id/test Send test notification
GET/POST /api/plugins List / upload WASM plugins
DELETE /api/plugins/:id Delete plugin
POST /api/plugins/:id/enable Enable plugin
POST /api/plugins/:id/disable Disable plugin
GET/POST /api/tunnels List / create reverse tunnels
DELETE /api/tunnels/:id Delete tunnel
GET /api/cache/stats Cache statistics
DELETE /api/cache Flush entire cache
DELETE /api/cache/host/:host Flush cache for a host
DELETE /api/cache/key Flush a specific cache key
GET /api/audit-log Admin action audit log
GET /api/cluster/status Cluster health overview
GET /api/cluster/nodes List cluster nodes
GET /api/cluster/nodes/:id Get cluster node details
POST /api/cluster/token Generate node join token
POST /api/cluster/nodes/remove Remove a node from cluster
GET /api/crowdsec/status CrowdSec integration status
GET /api/crowdsec/decisions Active CrowdSec decisions
DELETE /api/crowdsec/decisions/:id Delete a CrowdSec decision
POST /api/crowdsec/test Test LAPI connectivity
GET/PUT /api/crowdsec/config Get / update CrowdSec config
GET /api/crowdsec/stats CrowdSec statistics
GET /api/crowdsec/events CrowdSec event log
WS /ws/events Real-time security event stream
WS /ws/logs Real-time access/attack log stream
WS /ws/tunnel Reverse tunnel WebSocket endpoint

Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feat/my-feature)
  3. Make your changes with tests
  4. Run cargo test and cargo clippy
  5. Submit a pull request

Development Setup

# Install Rust (https://rustup.rs)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Install Node.js 18+ for the admin UI
# Install PostgreSQL 16+

# Start a local DB
createdb prx_waf && createuser -s prx_waf

# Build everything
cargo build

# Build admin UI
cd web/admin-ui && npm install && npm run build

Fuzzing

Every parser reachable from a request is fuzzed with cargo-fuzz; the harnesses, seed corpora and target rationale live in fuzz/, and .github/workflows/fuzz.yml runs a short regression on each PR plus a weekly soak. If you touch a parser or a detector, run cd fuzz && cargo +nightly fuzz run <target> -- -max_total_time=60 before opening the PR.

Code Structure

  • All detection logic lives in crates/waf-engine/src/checks/
  • New checks implement the Check trait from checks/mod.rs
  • Database schema changes require a new migration in migrations/
  • Admin UI components live in web/admin-ui/src/views/

Links

License

Copyright (c) 2026 Span Brain LLC.

Licensed under either of

at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

About

High-performance Web Application Firewall built on Pingora. OWASP CRS, GeoIP (ip2region), bot detection, API security, and 644+ built-in rules.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

12 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages