Caddy WAF (Regex Rules, IP and DNS filtering, Rate Limiting, GeoIP, Tor, Anomaly Detection)
See the codeA Web Application Firewall middleware for the Caddy web server, written in Go.
http.handlers.waf — registered in Caddy's package registry, so the module is selectable on the download pagegithub.com/fabriziosalmi/caddy-wafv0.4.16 (see caddywaf.go — const wafVersionDefault)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 | Redaction of sensitive values in logs (by key name and by value content), on by default; disable with redact_sensitive_data off. |
| 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. |
regexp (RE2). No catastrophic backtracking.atomic.Int64 stored in a sync.Map.io.LimitReader (max_request_body_size, default 10 MiB) and restored with io.MultiReader so downstream handlers still see the full body.max_response_body_size (default 10 MiB). Beyond that — or as soon as the upstream flushes — the WAF releases what it holds and streams the rest, so memory never scales with the response size.unsafe.String to avoid an allocation per request.geoip_fail_open controls whether a GeoIP lookup failure blocks the request or allows it through.ServeHTTP installs a deferred recovery that returns 500 Internal Server Error on 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 | bash
The 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":5}
INFO http.handlers.waf Tor exit nodes updated {"count":1093}
INFO WAF middleware version {"version":"v0.4.16"}
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.mod declares go 1.26.0, propagated from caddy/v2 which requires it)github.com/caddyserver/caddy/v2 v2.11.4)xcaddy for building Caddy with pluginsgo install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
xcaddy build --with github.com/fabriziosalmi/caddy-waf
./caddy list-modules | grep waf # expect: http.handlers.waf
curl -fsSL -H "Pragma: no-cache" \
https://raw.githubusercontent.com/fabriziosalmi/caddy-waf/refs/heads/main/install.sh | bash
git 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 run
caddy add-package[!IMPORTANT] Prefer
xcaddyor the container image. Caddy's maintainers have proposed movingadd-package,remove-packageandupgradeout of Caddy's core to discourage their use (caddyserver/caddy#7010): the commands call Caddy's shared build server, and using them in CI/CD is an anti-pattern. Raised for this project in #138 by a Caddy maintainer.The command works today and the module remains registered, so this section stays accurate. Treat it as a convenience for one-off, hand-operated installs — not as the way to build or deploy caddy-waf.
The module is registered in Caddy's package registry, so an existing Caddy v2.7+ binary can pull it in without a Go toolchain:
caddy add-package github.com/fabriziosalmi/caddy-waf
caddy list-modules | grep waf # expect: http.handlers.waf
It is also selectable on caddyserver.com/download. See docs/add-package-guide.md for version pinning, removal, and when to prefer xcaddy instead.
Images are published to GitHub Container Registry on every release tag, for linux/amd64 and linux/arm64:
docker pull ghcr.io/fabriziosalmi/caddy-waf:0.4.16
docker run --rm -p 8080:8080 ghcr.io/fabriziosalmi/caddy-waf:0.4.16
Tags are 0.4.16, 0.4 and latest — note there is no v prefix, unlike the Go module version. Pin an exact version in anything you deploy. See docs/docker.md for volumes, Compose, and hot reload.
{
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.
fabriziosalmi.github.io/caddy-waf — the same pages as below, with full-text search and cross-linking. The Markdown under docs/ remains the source and stays readable on GitHub.
| Document | Topic |
|---|---|
docs/introduction.md | What the middleware does and where it sits in the request pipeline. |
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 | Installing with caddy add-package. |
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
├── debug_waf.go Debug helpers for rule evaluation
├── assets.go Embedded dashboard assets (build tag)
├── assets_stub.go No-op asset provider for builds without the dashboard
├── 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
├── ui/ Dashboard, with third-party assets vendored same-origin
├── 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 container
The 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.
Running caddy-waf in production? I offer paid support, custom rule development, and security consulting — WAF tuning, hardening, TLS automation, and cloud detection & alerting. Reach out: [email protected].
Pull requests are welcome. The project values:
rules/ using the documented JSON schema.docs/configuration.md and the relevant topic page).See CONTRIBUTING.md for the workflow and CODE_OF_CONDUCT.md.
Please do not open a public issue for a vulnerability. Use private vulnerability reporting — it is enabled on this repository, so it works for anyone without special permissions — or email [email protected]. Full policy in SECURITY.md.
Only the latest release receives security fixes; there are no backport branches. Published advisories are listed under Security → Advisories.
Upgrade note: v0.3.3 and earlier are affected by GHSA-gfj3-cmff-q8wh, a high-severity unauthenticated denial of service (unbounded response buffering, CVSS 7.5). Fixed in v0.3.4.
AGPL-3.0. See LICENSE.
6,486 followers · starred Jan 2025
680 followers · starred Oct 2025
104 followers · starred Jan 2025
442 followers · starred Jun 2026
Caddy WAF (Regex Rules, IP and DNS filtering, Rate Limiting, GeoIP, Tor, Anomaly Detection)
See the codeA Web Application Firewall middleware for the Caddy web server, written in Go.
http.handlers.waf — registered in Caddy's package registry, so the module is selectable on the download pagegithub.com/fabriziosalmi/caddy-wafv0.4.16 (see caddywaf.go — const wafVersionDefault)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 | Redaction of sensitive values in logs (by key name and by value content), on by default; disable with redact_sensitive_data off. |
| 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. |
regexp (RE2). No catastrophic backtracking.atomic.Int64 stored in a sync.Map.io.LimitReader (max_request_body_size, default 10 MiB) and restored with io.MultiReader so downstream handlers still see the full body.max_response_body_size (default 10 MiB). Beyond that — or as soon as the upstream flushes — the WAF releases what it holds and streams the rest, so memory never scales with the response size.unsafe.String to avoid an allocation per request.geoip_fail_open controls whether a GeoIP lookup failure blocks the request or allows it through.ServeHTTP installs a deferred recovery that returns 500 Internal Server Error on 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 | bash
The 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":5}
INFO http.handlers.waf Tor exit nodes updated {"count":1093}
INFO WAF middleware version {"version":"v0.4.16"}
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.mod declares go 1.26.0, propagated from caddy/v2 which requires it)github.com/caddyserver/caddy/v2 v2.11.4)xcaddy for building Caddy with pluginsgo install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
xcaddy build --with github.com/fabriziosalmi/caddy-waf
./caddy list-modules | grep waf # expect: http.handlers.waf
curl -fsSL -H "Pragma: no-cache" \
https://raw.githubusercontent.com/fabriziosalmi/caddy-waf/refs/heads/main/install.sh | bash
git 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 run
caddy add-package[!IMPORTANT] Prefer
xcaddyor the container image. Caddy's maintainers have proposed movingadd-package,remove-packageandupgradeout of Caddy's core to discourage their use (caddyserver/caddy#7010): the commands call Caddy's shared build server, and using them in CI/CD is an anti-pattern. Raised for this project in #138 by a Caddy maintainer.The command works today and the module remains registered, so this section stays accurate. Treat it as a convenience for one-off, hand-operated installs — not as the way to build or deploy caddy-waf.
The module is registered in Caddy's package registry, so an existing Caddy v2.7+ binary can pull it in without a Go toolchain:
caddy add-package github.com/fabriziosalmi/caddy-waf
caddy list-modules | grep waf # expect: http.handlers.waf
It is also selectable on caddyserver.com/download. See docs/add-package-guide.md for version pinning, removal, and when to prefer xcaddy instead.
Images are published to GitHub Container Registry on every release tag, for linux/amd64 and linux/arm64:
docker pull ghcr.io/fabriziosalmi/caddy-waf:0.4.16
docker run --rm -p 8080:8080 ghcr.io/fabriziosalmi/caddy-waf:0.4.16
Tags are 0.4.16, 0.4 and latest — note there is no v prefix, unlike the Go module version. Pin an exact version in anything you deploy. See docs/docker.md for volumes, Compose, and hot reload.
{
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.
fabriziosalmi.github.io/caddy-waf — the same pages as below, with full-text search and cross-linking. The Markdown under docs/ remains the source and stays readable on GitHub.
| Document | Topic |
|---|---|
docs/introduction.md | What the middleware does and where it sits in the request pipeline. |
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 | Installing with caddy add-package. |
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
├── debug_waf.go Debug helpers for rule evaluation
├── assets.go Embedded dashboard assets (build tag)
├── assets_stub.go No-op asset provider for builds without the dashboard
├── 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
├── ui/ Dashboard, with third-party assets vendored same-origin
├── 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 container
The 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.
Running caddy-waf in production? I offer paid support, custom rule development, and security consulting — WAF tuning, hardening, TLS automation, and cloud detection & alerting. Reach out: [email protected].
Pull requests are welcome. The project values:
rules/ using the documented JSON schema.docs/configuration.md and the relevant topic page).See CONTRIBUTING.md for the workflow and CODE_OF_CONDUCT.md.
Please do not open a public issue for a vulnerability. Use private vulnerability reporting — it is enabled on this repository, so it works for anyone without special permissions — or email [email protected]. Full policy in SECURITY.md.
Only the latest release receives security fixes; there are no backport branches. Published advisories are listed under Security → Advisories.
Upgrade note: v0.3.3 and earlier are affected by GHSA-gfj3-cmff-q8wh, a high-severity unauthenticated denial of service (unbounded response buffering, CVSS 7.5). Fixed in v0.3.4.
AGPL-3.0. See LICENSE.
6,486 followers · starred Jan 2025
680 followers · starred Oct 2025
104 followers · starred Jan 2025
442 followers · starred Jun 2026