A Web Application Firewall middleware for the Caddy web server, written in Go.
- Module ID:
http.handlers.waf - Go module path:
github.com/fabriziosalmi/caddy-waf - Current version:
v0.3.0(seecaddywaf.go—const wafVersion) - License: AGPL-3.0
caddy-waf is an HTTP handler middleware that inspects requests and responses across four well-defined phases, applies a regular-expression rule set with anomaly scoring, enforces IP/DNS/ASN/country blacklists and whitelists, performs token-bucket-style rate limiting, and exposes a JSON metrics endpoint.
The middleware is implemented as a single Caddy module registered under the ID http.handlers.waf. It can be configured through the Caddyfile or directly via JSON.
| Capability | Implementation |
|---|---|
| Regex rule engine | Go regexp package (RE2, linear-time guarantee). Compiled patterns are cached per rule ID. |
| Multi-phase inspection | Phase 1 (request headers and pre-request checks), Phase 2 (request body), Phase 3 (response headers), Phase 4 (response body). |
| Anomaly scoring | Each rule contributes its score to a per-request total; requests are blocked when the total reaches anomaly_threshold. |
| Explicit actions | Rule mode of block or log. block short-circuits the request; log records the match and continues. |
| IP blacklist | Plain IPs and CIDR ranges (IPv4 and IPv6) stored in a prefix trie (go-iptrie). |
| DNS blacklist | Exact-match (case-insensitive) host lookup. |
| GeoIP country block / whitelist | MaxMind GeoLite2 Country MMDB. |
| ASN block | MaxMind GeoLite2 ASN MMDB. |
| Tor exit-node block | Periodic fetch from https://check.torproject.org/torbulkexitlist. |
| Rate limiting | Per-IP, sliding-window, optional per-path matching with regex patterns. |
| Custom block responses | Per-status-code response with custom Content-Type, headers, and body (inline or from file). |
| Sensitive data redaction | Optional redaction of sensitive query parameters and log fields. |
| Hot reload | fsnotify watchers on rule files, IP blacklist, and DNS blacklist. |
| Metrics endpoint | JSON document exposed at the configured metrics_endpoint path. |
| Asynchronous logging | Buffered log channel with synchronous fallback when the buffer is full. |
- Linear-time regex: rules are compiled by Go's
regexp(RE2). No catastrophic backtracking. - Wait-free counters: per-rule hit counts use
atomic.Int64stored in async.Map. - Bounded body reads: request bodies are read through
io.LimitReader(max_request_body_size, default 10 MiB) and restored withio.MultiReaderso downstream handlers still see the full body. - Zero-copy body string: the body is exposed to rule matching via
unsafe.Stringto avoid an allocation per request. - Circuit breaker for GeoIP:
geoip_fail_opencontrols whether a GeoIP lookup failure blocks the request or allows it through. - Panic recovery:
ServeHTTPinstalls a deferred recovery that returns500 Internal Server Erroron panic.
The fastest path to a working build:
curl -fsSL -H "Pragma: no-cache" \
https://raw.githubusercontent.com/fabriziosalmi/caddy-waf/refs/heads/main/install.sh | bashThe script ensures Go and xcaddy are installed, clones the repository, downloads the GeoLite2 database, builds Caddy with the caddy-waf module, and starts the server.
A representative provisioning log:
INFO Provisioning WAF middleware {"log_level":"info","log_path":"debug.json","log_json":true,"anomaly_threshold":20}
INFO http.handlers.waf Tor exit nodes updated {"count":1093}
INFO WAF middleware version {"version":"v0.3.0"}
INFO Rate limit configuration {"requests":100,"window":10,"cleanup_interval":300,"paths":["/api/v1/.*"],"match_all_paths":false}
WARN GeoIP database not found. Country blacklisting/whitelisting will be disabled {"path":"GeoLite2-Country.mmdb"}
INFO IP blacklist loaded {"path":"ip_blacklist.txt","valid_entries":223770,"invalid_entries":0,"total_lines":223770}
INFO DNS blacklist loaded {"path":"dns_blacklist.txt","valid_entries":854479,"total_lines":854479}
INFO WAF rules loaded successfully {"total_rules":33,"rule_counts":"Phase 1: 17 rules, Phase 2: 16 rules, Phase 3: 0 rules, Phase 4: 0 rules, "}
INFO WAF middleware provisioned successfully
- Go 1.25 or newer (
go.moddeclaresgo 1.25) - Caddy v2.10.x or newer (current build uses
github.com/caddyserver/caddy/v2 v2.10.2) xcaddyfor building Caddy with plugins
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
xcaddy build --with github.com/fabriziosalmi/caddy-waf
./caddy list-modules | grep waf # expect: http.handlers.wafcurl -fsSL -H "Pragma: no-cache" \
https://raw.githubusercontent.com/fabriziosalmi/caddy-waf/refs/heads/main/install.sh | bashgit clone https://github.com/fabriziosalmi/caddy-waf.git
cd caddy-waf
go mod tidy
wget https://git.io/GeoLite2-Country.mmdb # optional, only for GeoIP
xcaddy build --with github.com/fabriziosalmi/caddy-waf=./
./caddy fmt --overwrite
./caddy runThis module is not registered in Caddy's official package registry; caddy add-package github.com/fabriziosalmi/caddy-waf will fail with HTTP 400: ... is not a registered Caddy module package path. Use one of the build options above. See docs/add-package-guide.md for details.
{
auto_https off
admin localhost:2019
}
:8080 {
log {
output stdout
format console
level INFO
}
route {
waf {
metrics_endpoint /waf_metrics
rule_file rules.json
ip_blacklist_file ip_blacklist.txt
dns_blacklist_file dns_blacklist.txt
}
@wafmetrics path /waf_metrics
handle @wafmetrics {
# Falls through to the WAF handler, which serves metrics as JSON.
}
handle {
respond "Hello world!" 200
}
}
}A fully annotated example is provided in Caddyfile and caddyfile.example.
| Document | Topic |
|---|---|
docs/installation.md |
All installation methods. |
docs/configuration.md |
Caddyfile and JSON directives, request lifecycle, blocking precedence. |
docs/rules.md |
rules.json schema, target identifiers, regex semantics. |
docs/blacklists.md |
IP and DNS blacklist file formats. |
docs/ratelimit.md |
Rate-limit block, path matching, behavior. |
docs/geoblocking.md |
Country block / whitelist, ASN block, fallback behavior. |
docs/attacks.md |
Attack categories addressed by the bundled rule sets. |
docs/dynamicupdates.md |
File watchers, reload semantics, scope of each reload. |
docs/metrics.md |
/waf_metrics JSON schema. |
docs/prometheus.md |
Bridging the JSON metrics to Prometheus and Grafana. |
docs/caddy-waf-elk.md |
Shipping logs to an ELK stack with Filebeat. |
docs/scripts.md |
Helper Python scripts for rule and blacklist generation. |
docs/testing.md |
Running the bundled test.py suite. |
docs/docker.md |
Building and running with Docker / Docker Compose. |
docs/add-package-guide.md |
Status of caddy add-package registration. |
docs/caddytest.md |
The caddytest.py traffic-generation tool. |
.
├── caddywaf.go Module registration, lifecycle, file watchers, metrics endpoint
├── handler.go ServeHTTP, phase dispatch, blocking decisions
├── config.go Caddyfile directive parsing
├── rules.go Rule loading, validation, regex caching, hit accounting
├── request.go Target extraction (URI, headers, body, JSON paths, ...)
├── response.go Response recorder, block writer
├── blacklist.go IP and DNS blacklist loaders and lookups
├── ratelimiter.go Per-IP / per-path sliding-window rate limiter
├── geoip.go MaxMind country and ASN lookups, with optional cache
├── tor.go Periodic Tor exit-node list fetcher
├── logging.go Async log worker, sensitive-data redaction
├── helpers.go IP parsing helpers
├── types.go Public types (Middleware, Rule, RateLimit, ...)
├── doc.go Package documentation
├── rules.json Default rule set (used by Caddyfile)
├── rules/ Modular rule files by category
├── ip_blacklist.txt Default IP blacklist
├── dns_blacklist.txt Default DNS blacklist
├── tor_blacklist.txt Tor exit-node cache (auto-managed)
├── docs/ Documentation
└── *_test.go Unit and integration tests
make test # go test -v ./...
make it # go test -v ./... -tags=it (integration)
make lint # golangci-lint run
make test-integration # runs test.py inside a python:3.9-slim containerThe repository also ships Python suites covering offensive payloads (test.py), traffic generation (caddytest.py), and benchmarking (benchmark.py). See docs/testing.md and docs/caddytest.md.
Pull requests are welcome. The project values:
- Tests that exercise the change.
- Rule contributions placed under
rules/using the documented JSON schema. - Documentation kept in sync with code (every directive change must be reflected in
docs/configuration.mdand the relevant topic page).
See CONTRIBUTING.md for the workflow and CODE_OF_CONDUCT.md.
For vulnerability disclosure see SECURITY.md. Reports may be sent to fabrizio.salmi@gmail.com or as a private GitHub Security Advisory; please do not open a public issue.
AGPL-3.0. See LICENSE.