warden: A Single-Binary Go Reverse-Proxy WAF

TL;DR: warden is a self-hosted WAF for small sites that are bothered by CC floods and scanners, but cannot justify a commercial WAF and do not want to change DNS for a cloud one. Drop a single binary on the server and point your Nginx traffic at it. Current release is v1.0.1, licensed Apache-2.0, shipping for Windows and Linux (amd64 + arm64).

1. The problem it targets

Small sites - company homepages, school portals, internal business systems - are drained by two kinds of traffic:

  1. CC floods and scraping: many IPs hammering hot pages (news listings, detail pages, search endpoints). Every single request looks legitimate; together they saturate the backend.
  2. Scanner probing: path scans for /wp-admin, /.env, /phpmyadmin, plus the fingerprints of tools like sqlmap and nikto.

Every off-the-shelf option has a catch: commercial WAFs cost money; cloud WAFs require a DNS change and route traffic through someone else; Nginx's native limit_req can rate limit but is limited at telling real users from scripts - rate limiting is allow or deny, with nothing in between.

warden's position: one small self-hosted program covering L4-L7 protection, CC challenges and observability. Its guiding principle is "better one extra CAPTCHA than blocking a real user" - suspected scripts get a challenge first, and passing it issues a trust credential that puts them on the fast path.

2. Three choices made for deployability

The most interesting part of this project is not the feature list but how far it pushes down deployment complexity.

a. The frontend is embedded with go:embed

The admin dashboard is a single-file Vue3 + Element Plus + ECharts app, embedded directly into the executable with go:embed: no separate frontend deployment, no static location in Nginx. Third-party libraries are vendored locally in web/vendor/ rather than loaded from a CDN, so it works on isolated networks.

// edit web/admin.html, rebuild, and the assets are baked in
go build -o warden ./cmd/warden

b. Pure-Go SQLite, no cgo

It uses modernc.org/sqlite instead of the cgo-based mattn driver. The direct benefit: cross-compilation is painless, so you can produce Linux binaries from Windows with no cross toolchain.

CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o warden-linux-arm64 ./cmd/warden

One release zip contains warden.exe (Windows/amd64), warden (Linux/amd64) and warden-linux-arm64, all flat in the root, and run.sh picks the right one via uname -m.

c. Every in-memory state has a TTL and a sweeper

Token buckets, trusted IPs, behavior state and offense records all expire and are cleaned up by background goroutines, so memory does not grow without bound. That is what makes a self-hosted daemon safe to deploy and forget.

3. How a request travels

client --> [Nginx / LB, optional] --> warden :81 --> backend :8002

Internally, two layers:

TCP layer: ConnLimitListener applies a token bucket at the accept stage and drops excess connections outright, before any HTTP work. That is where the most resource-hungry connection floods get stopped.

HTTP middleware chain, outside in:

1. IP blocklist     -- hit -> block
2. URL allow/deny   -- hit -> allow / block
3. IP allowlist     -- hit -> pass through (skips every check below)
4. CC protection    -- flood / behavior / rate -> CAPTCHA challenge
5. Rate limiting    -- hot path / site-wide / subnet -> 429
6. Coraza WAF       -- OWASP CRS rule detection
7. Multi-site router-- reverse proxy by Host

The order is the strategy: allowlisted IPs bypass everything, and the expensive check (Coraza) sits near the end, so traffic already blocked upstream never reaches the rule engine.

4. How CC protection decides "this is a script"

This is where warden differs most from plain rate limiting. It does not rely on a single threshold; it stacks several signals:

MechanismSignal
New-IP flood detectionSliding-window ratio of new vs. known IPs; above the threshold it is a flood and every new IP must pass a CAPTCHA
Behavior detectionSuspiciously uniform request intervals, or hitting only one or two paths forever -> script
Trusted-IP fast pathCumulative visits above a threshold promote the IP to trusted, with its own high-quota bucket; persisted to SQLite
Trusted-session fast pathPassing a CAPTCHA issues a cookie, so multiple users behind one IP do not interfere
Per-IP rate limitSeparate token bucket for untrusted IPs; exceeding it raises a CAPTCHA instead of dropping the connection
Shared token bucketTotal untrusted capacity guard; under congestion it self-recovers via CAPTCHAs

A few details show it was honed against real traffic:

  • Static assets are exempt: images, JS, CSS and fonts pass straight through, so one article loading dozens of resources is not mistaken for an attack.
  • CAPTCHAs are reused per (session, IP): regenerating does not clobber the code the user is currently typing.
  • Search-engine allowlist: crawler user agents get rate limiting instead of CAPTCHAs, so indexing is unaffected.
  • Escalation needs two conditions: an offense count and sustained offending beyond offender_persist_sec before kernel-level blocking kicks in - a one-off IP should not create a firewall rule and bloat the rule table.

There is also IP origin blocking backed by an offline xdb database: foreign IPs and cloud/IDC ranges can be blocked independently. Real users rarely come from a Tencent Cloud or Alibaba Cloud data center, so this is effective against bot traffic - and it makes no external requests, so there is no third-party API dependency.

5. Up and running in three minutes

With a release zip, no compilation needed:

mkdir -p /opt/warden && cd /opt/warden
unzip ~/warden-v1.0.1.zip
chmod +x run.sh warden warden-linux-arm64   # zip does not preserve executable bits
./run.sh

Building it yourself (Go 1.23+):

# Windows
$env:GOPROXY = "https://goproxy.cn,https://goproxy.io,direct"
go mod tidy
go build -o warden.exe ./cmd/warden

# Linux
export GOPROXY=https://goproxy.cn,https://goproxy.io,direct
go build -o warden ./cmd/warden
./warden -config config.json

The minimal config is two lines - it sits in front of your backend:

{
  "listen": ":81",
  "backend": "http://127.0.0.1:8002"
}

Verify:

curl http://127.0.0.1:81/healthz   # health check
curl -I  http://127.0.0.1:81/      # should be proxied to the backend

The dashboard defaults to http://127.0.0.1:9090 and shows live blocking counters, QPS/block-rate trends, a breakdown of block categories, CPU and memory, paginated attack logs and the trusted IP list.

Three deployment shapes to choose from:

OptionPathWhen
A. Nginx in front (recommended):443 -> Nginx (TLS) -> warden:81 -> backendYou need TLS and already run Nginx
B. warden exposed directly:80 -> warden -> backendYou want one hop less (on Windows, port 80 needs admin)
C. Multi-siteRoute by Host to different upstreamsOne machine proxying several sites

For long-term operation, register it as a service with systemd (Linux) or NSSM (Windows); the README has a ready-made unit file.

6. Five things to know before going live

a. Observe with DetectionOnly, then switch to On

Set SecRuleEngine DetectionOnly in rules/coraza.conf, watch for false positives for a while, and only then switch to On. Do the same with CC protection: start with CAPTCHA challenges only, check the real-user pass rate, then tighten thresholds gradually. Going straight to On is the number one cause of a WAF blocking legitimate traffic.

b. Editing config.json has no effect

Config resolution is SQLite config table first, config.json as fallback. The first start seeds the database from config.json; after that the database wins. Change settings in the dashboard and restart.

c. Nginx on Windows caps out at 1024 connections

The Windows build of Nginx uses the select() event model, so a single worker handles roughly 1024 concurrent connections no matter how large worker_connections is. For higher concurrency, run Nginx on Linux or WSL2, or let warden listen directly (Go uses IOCP on Windows and has no such limit).

d. Align upstream keepalive timeouts

The idle-connection reclaim time from warden to the backend must be shorter than the backend's own (for example Tomcat's connectionTimeout), otherwise warden reuses connections the backend already closed, causing connection reset and intermittent 502s.

e. Firewall blocking is off by default - keep it that way at first

An early version created two netsh rules per IP, which under attack volume quickly accumulated into tens of thousands; Windows Firewall recompiles the entire rule table on every add or remove, so operations got slower and slower. It is therefore disabled by default now, and enabling it means committing to periodic rule cleanup.

7. Two engineering details worth stealing

The dashboard runs in the same process as the WAF, so polling overhead lands directly on the serving path. Two optimizations address that:

  • Memory stats avoid runtime.ReadMemStats: it triggers a stop-the-world pause, measured at about 8.7 microseconds per call; reading the same metric through runtime/metrics takes about 0.3 microseconds (roughly 27x faster) with identical values. The dashboard polls /api/stats every 3 seconds, which amplifies the difference.
  • CPU usage is sampled continuously: GetSystemTimes and /proc/stat return cumulative values since boot, so a rate needs two samples. Sampling only when the endpoint is called means that after leaving the dashboard for a while, the first reading covers the whole gap - possibly an average over several minutes. A background loop samples every second and caches; the endpoint only reads the cache.

8. Who it is for, and who it is not

For: company homepages, school and government sites, internal business systems - cases where the backend is not very robust, traffic patterns are fairly stable, and nobody is doing security ops full time. Also for anyone who wants to self-host and fully control their data and rules.

Not for: teams needing enterprise-grade rule operations, volumetric DDoS scrubbing (that belongs at the ISP or CDN layer), or anyone expecting install-and-forget. The README says it plainly: this is application-layer protection and does not replace system patching, least privilege, or secure coding in the backend.

9. Project facts

  • License: Apache-2.0 (includes an explicit patent grant; commercial use and closed-source integration allowed)
  • Stack: Go 1.23+, OWASP Coraza v3, pure-Go SQLite, Vue3 + Element Plus + ECharts
  • Current version: v1.0.1 (single cross-platform package)
  • Gitee: gitee.com/jxw1111/warden
  • GitHub: github.com/xwjiang2003/warden

Issues and PRs are welcome. Contributions are licensed under Apache-2.0 by default; please include a Signed-off-by line (DCO) in your commit message.

Further reading

Feedback

This site is front-end only — no backend, no accounts — so feedback goes to GitHub Issues, Discussions, or email.

Paste it into the issue or email to help track it down — it never includes anything you typed

Also: Browse existing issues · Email feedback (no GitHub account needed): 278975598@qq.com