Devops Web Security 1 min read

Stop Wrestling with ModSecurity! Use caddy-waf Instead

B
Bright Coding
Author
Share:
Stop Wrestling with ModSecurity! Use caddy-waf Instead
Advertisement

Stop Wrestling with ModSecurity! Use caddy-waf Instead

Your production server just got pummeled by another SQL injection probe. Again. You're staring at yet another 3 AM alert, wondering why your Web Application Firewall feels like it's fighting you harder than the attackers. If you've ever wrestled with ModSecurity's arcane rule syntax, battled nginx's compiled module hell, or watched your WAF consume more RAM than your actual application, I've got news that'll make you sit up straight.

There's a better way. And it's hiding in plain sight.

Enter caddy-waf — a modern, Go-powered WAF middleware that snaps directly onto Caddy, the web server that already handles HTTPS automatically. No more compiled nightmares. No more XML rule configurations from 2003. Just pure, linear-time regex performance with anomaly scoring that actually makes sense. The secret weapon top infrastructure engineers are quietly deploying? It's this repository, and by the end of this article, you'll know exactly why everyone's abandoning legacy WAFs for this approach.

Ready to protect your applications without losing your sanity? Let's dive deep.


What is caddy-waf?

caddy-waf is a Web Application Firewall middleware built specifically for the Caddy web server, written entirely in Go by developer Fabrizio Salmi. Currently at version v0.3.0, this module registers under the ID http.handlers.waf and transforms any Caddy instance into a fully-featured security gateway.

But here's what makes it genuinely exciting: unlike traditional WAFs that bolt onto your stack like an afterthought, caddy-waf is native Caddy middleware. It inspects requests and responses across four distinct phases — request headers, request body, response headers, and response body — applying a sophisticated rule engine with anomaly scoring that would make enterprise security vendors jealous.

The project is trending right now because it solves a problem that's plagued developers for decades: how do you get serious security without serious complexity? Caddy itself revolutionized web serving with automatic HTTPS and dead-simple configuration. caddy-waf extends that philosophy to application security. No separate appliances. No proxy chains that add latency. Just clean, configurable protection that lives where your traffic already flows.

Under the hood, the engineering is deliberately rigorous. The regex engine uses Go's standard regexp package (RE2), which guarantees linear-time execution — meaning no catastrophic backtracking that attackers exploit to DoS your WAF itself. Compiled patterns are cached per rule ID. Hit counters use wait-free atomic.Int64 operations in a sync.Map. Body reads are bounded through io.LimitReader with restoration via io.MultiReader, so downstream handlers never lose access to the full payload.

This isn't a toy project. With comprehensive CI/CD pipelines (Tests, CodeQL, Build/Run/Validate), AGPL-3.0 licensing, and active maintenance, caddy-waf represents a mature, production-ready approach to web application defense.


Key Features That Make caddy-waf Insane

Let's dissect what separates caddy-waf from the security theater you'll find elsewhere:

Four-Phase Inspection Architecture

Most WAFs look at your request once and call it a day. caddy-waf operates across Phase 1 (request headers and pre-request checks), Phase 2 (request body deep inspection), Phase 3 (response headers), and Phase 4 (response body). This means exfiltration attempts in responses get caught too — not just incoming attacks.

Linear-Time Regex Engine with Anomaly Scoring

Each rule carries a configurable score that contributes to a per-request total. When the cumulative score hits your anomaly_threshold, the request gets blocked. This modular scoring approach eliminates binary "match/block" fragility. Subtle attack indicators that individually seem innocent can collectively trigger protection — exactly how modern threats actually behave.

Multi-Layer Blacklist System

  • IP Blacklists: Plain IPs and CIDR ranges (IPv4 and IPv6) stored in a blazing-fast prefix trie via go-iptrie
  • DNS Blacklists: Case-insensitive exact-match host lookups
  • GeoIP Country Block/Whitelist: MaxMind GeoLite2 Country MMDB integration
  • ASN Blocking: MaxMind GeoLite2 ASN database support
  • Tor Exit-Node Blocking: Automatic periodic fetching from https://check.torproject.org/torbulkexitlist

Token-Bucket Rate Limiting

Per-IP sliding-window rate limiting with optional per-path regex matching. Configure different thresholds for /api/* versus your static assets. Cleanup intervals keep memory bounded.

Hot Reload Without Restarts

fsnotify watchers monitor rule files, IP blacklists, and DNS blacklists for changes. Update your defenses in real-time without dropping connections. This is the kind of operational elegance that separates professional infrastructure from weekend projects.

Production-Ready Observability

  • JSON metrics endpoint at your configured path for programmatic monitoring
  • Asynchronous logging with buffered channels and synchronous fallback when buffers saturate
  • Sensitive data redaction for query parameters and log fields
  • Custom block responses with per-status-code Content-Type, headers, and body (inline or file-based)

Defensive Engineering

  • Circuit breaker for GeoIP: geoip_fail_open controls whether lookup failures block or allow traffic
  • Panic recovery: Deferred recovery in ServeHTTP returns clean 500s instead of crashing your server
  • Zero-copy body string: unsafe.String exposure to avoid per-request allocations

Use Cases Where caddy-waf Absolutely Dominates

1. API-First Startups Needing Immediate Protection

You've got a JSON API serving mobile clients. You need SQL injection defense, rate limiting by endpoint, and geo-blocking for compliance — yesterday. caddy-waf's Caddyfile configuration gets you from zero to protected in minutes, not days. The JSON rule schema maps cleanly to API attack patterns.

2. Multi-Tenant SaaS Platforms

Different tenants need different rule sets? The modular rules/ directory structure lets you compose rule files by category. Hot reload means you can push tenant-specific protections without restart orchestration across your fleet.

3. Edge-Deployed Microservices

Running Caddy as a lightweight edge proxy? caddy-waf's bounded memory usage (10 MiB default body limit, configurable) and wait-free counters mean you get enterprise-grade inspection without the enterprise-grade resource footprint. Perfect for container-constrained environments.

4. Tor-Aware Public Services

Operating a whistleblower platform, privacy-focused service, or just need to understand your Tor traffic? The automatic Tor exit-node list fetching with configurable blocking gives you precise control without manual list curation.

5. Compliance-Driven Geofencing

GDPR data residency requirements? Export control restrictions? The GeoIP country whitelist/blacklist with ASN blocking provides the geographic controls auditors expect, with geoip_fail_open ensuring you don't accidentally hard-fail during database updates.


Step-by-Step Installation & Setup Guide

Let's get caddy-waf running. You have three paths, from quickest to most controlled.

Option 1: The One-Liner (Quickest)

curl -fsSL -H "Pragma: no-cache" \
  https://raw.githubusercontent.com/fabriziosalmi/caddy-waf/refs/heads/main/install.sh | bash

This script automates everything: Go and xcaddy installation, repository cloning, GeoLite2 database download, module compilation, and server startup. Perfect for evaluation.

Option 2: Build with xcaddy (Recommended for Production)

# Install xcaddy if you haven't already
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest

# Build Caddy with caddy-waf embedded
xcaddy build --with github.com/fabriziosalmi/caddy-waf

# Verify the module loaded
./caddy list-modules | grep waf   # expect: http.handlers.waf

Option 3: Build from Source (Full Control)

# Clone the repository
git clone https://github.com/fabriziosalmi/caddy-waf.git
cd caddy-waf

# Resolve dependencies
go mod tidy

# Optional: Download GeoIP database for country/ASN features
wget https://git.io/GeoLite2-Country.mmdb

# Build with local source override
xcaddy build --with github.com/fabriziosalmi/caddy-waf=./

# Format and run
./caddy fmt --overwrite
./caddy run

Requirements Check

Component Minimum Version
Go 1.25
Caddy v2.10.x
xcaddy latest

Critical Note: caddy add-package will fail with HTTP 400 because this module isn't registered in Caddy's official package registry yet. Use the build methods above. See docs/add-package-guide.md for details.

Configuration Files Setup

Create your core files before starting:

# Default rule set (33 rules included)
cp rules.json your-rules.json

# IP blacklist (223K+ entries in default)
touch ip_blacklist.txt

# DNS blacklist (854K+ entries in default)
touch dns_blacklist.txt

REAL Code Examples from the Repository

Let's examine actual code and configuration from caddy-waf's documentation, with detailed explanations of how each piece works in practice.

Example 1: Minimal Caddyfile Configuration

This is the foundational configuration that gets a protected server running on port 8080:

{
    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
        }
    }
}

What's happening here? The route block establishes Caddy's handler chain. The waf directive configures our middleware with four key parameters: metrics_endpoint exposes operational data at /waf_metrics, rule_file points to our anomaly-scoring rules, and the two blacklist files handle IP and DNS blocking. The @wafmetrics named matcher with handle creates a dedicated path for metrics retrieval — note the comment indicating this falls through to the WAF handler itself, which recognizes this configured endpoint and serves JSON statistics. Finally, the catch-all handle block provides our protected application response. The auto_https off disables automatic TLS for this local demonstration; remove it for production.

Example 2: Installation and Verification Commands

The build process uses standard Go tooling with Caddy's plugin architecture:

# Install xcaddy, the Caddy plugin builder
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest

# Compile Caddy with caddy-waf module embedded
xcaddy build --with github.com/fabriziosalmi/caddy-waf

# Critical verification step: confirm module registration
./caddy list-modules | grep waf   # expect: http.handlers.waf

Why this matters: xcaddy is Caddy's official build tool for custom modules. It handles dependency resolution, version compatibility, and static compilation. The --with flag injects our WAF module into the final binary. The list-modules | grep verification isn't optional — it proves the module registered correctly with Caddy's plugin system. If this returns nothing, your WAF isn't loaded and requests pass uninspected.

Example 3: Automated Quick-Start Script

For rapid deployment or CI/CD pipelines:

curl -fsSL -H "Pragma: no-cache" \
  https://raw.githubusercontent.com/fabriziosalmi/caddy-waf/refs/heads/main/install.sh | bash

The -fsSL flags are deliberate: -f fails silently on server errors (no HTML error pages piped to bash), -s silences progress, -S still shows errors, and -L follows redirects. The Pragma: no-cache header prevents stale script versions from CDN or proxy caches. This one-liner downloads and executes the project's install script, which handles Go installation verification, xcaddy bootstrapping, repository cloning, GeoLite2 database acquisition, compilation, and server startup. Ideal for Docker builds or fresh VM provisioning.

Example 4: Source-Based Build with Local Modifications

When you need to customize the WAF code itself:

git clone https://github.com/fabriziosalmi/caddy-waf.git
cd caddy-waf
go mod tidy
wget https://git.io/GeoLite2-Country.mmdb
xcaddy build --with github.com/fabriziosalmi/caddy-waf=./
./caddy fmt --overwrite
./caddy run

The =./ syntax is the key: normally --with fetches remote modules. The =./ path override tells xcaddy to use your local directory instead. This enables custom rule engine modifications, private fork integration, or pre-release testing. go mod tidy ensures all transitive dependencies resolve. The GeoLite2 download is optional but essential for country/ASN features; without it, geo-blocking silently disables itself (check logs). caddy fmt --overwrite normalizes Caddyfile formatting before execution.

Example 5: Testing and Quality Assurance

The project includes comprehensive test automation:

make test            # Unit tests: go test -v ./...
make it              # Integration tests: go test -v ./... -tags=it
make lint            # Static analysis: golangci-lint run
make test-integration # Full Python suite in container

The test pyramid here is intentional: unit tests verify individual components (regex compilation, trie lookups, rate limiter math), integration tests exercise the full request lifecycle with -tags=it build constraint gating, and the Python suite validates against real attack payloads. The containerized test-integration ensures reproducible environment execution. Run make lint before any contribution — the project enforces clean code standards.


Advanced Usage & Best Practices

Anomaly Threshold Tuning

Start with anomaly_threshold: 20 (the default), then adjust based on your traffic profile. Legitimate APIs with complex query parameters may need higher thresholds. Public-facing forms with high attack surface might tolerate lower values. Monitor /waf_metrics to identify rule hit distributions.

Rule File Organization

The rules/ directory supports modular organization. Separate rules by:

  • Attack category (sqli.json, xss.json, lfi.json)
  • Severity (critical.json, warning.json)
  • Application layer (api-rules.json, webapp-rules.json)

This enables targeted hot reloads — update SQL injection defenses without touching your entire rule set.

Rate Limiter Path Strategy

Use regex patterns in paths to apply stricter limits to expensive endpoints:

{
  "requests": 10,
  "window": 60,
  "paths": ["/api/v1/login", "/api/v1/password-reset"]
}

Authentication endpoints warrant tighter restrictions than read-only APIs.

Memory Optimization

The default max_request_body_size of 10 MiB protects against memory exhaustion. For file upload endpoints, increase this selectively in your route configuration, or handle uploads through a separate, uninspected handler.

Metrics-Driven Tuning

Export the JSON metrics to Prometheus via the documented bridge. Correlate rule hits with application error rates. A rule with 10,000 hits and zero false positives deserves a higher score; one triggering on legitimate traffic needs refinement.


Comparison with Alternatives

Feature caddy-waf ModSecurity (nginx) Cloudflare WAF AWS WAF
Deployment Single binary, self-hosted Compiled module + complex config SaaS, DNS-dependent AWS ecosystem locked
Configuration Caddyfile/JSON, hot reload SecRules, restart required Web UI/API AWS Console/API
Regex Engine RE2, linear-time guaranteed PCRE, backtracking risk Proprietary Proprietary
Anomaly Scoring Built-in, per-request Available (paranoia mode) Rate-based rules Rate-based rules
GeoIP MaxMind MMDB, self-hosted Third-party modules Built-in Built-in
Tor Blocking Automatic list fetching Manual configuration Enterprise tier Manual IP lists
Latency Minimal (same process) Minimal (same process) Network hop Network hop
Cost Free (AGPL-3.0) Free (open source) $20-200+/mo $5-100+/mo + requests
Data Sovereignty Full control Full control Cloud-dependent AWS-dependent

The verdict: caddy-waf wins for teams wanting modern configuration, zero external dependencies, and complete data control without enterprise pricing. Cloud solutions trade simplicity for vendor lock-in. ModSecurity trades maturity for operational friction. caddy-waf sits in the sweet spot: modern engineering, self-hosted sovereignty, Caddy's legendary ergonomics.


FAQ

Is caddy-waf production-ready?

Yes. Version v0.3.0 includes comprehensive test coverage, CI/CD validation, panic recovery, circuit breakers, and bounded resource usage. The anomaly scoring approach reduces false-positive risk compared to binary rule systems.

Can I use caddy-waf without GeoIP databases?

Absolutely. GeoIP features gracefully disable themselves when databases are absent, with a clear log warning. All other protections (regex rules, IP/DNS blacklists, rate limiting, Tor blocking) function normally.

How does the anomaly scoring prevent false positives?

Individual rules contribute partial scores rather than immediate blocks. A single suspicious header might add 5 points to a threshold of 20. Multiple indicators must align before blocking occurs — mimicking how security analysts actually evaluate threats.

What's the performance impact?

Minimal. Wait-free atomic counters, compiled regex caching, zero-copy body exposure, and bounded reads keep per-request overhead low. The RE2 linear-time guarantee prevents regex-based DoS attacks against the WAF itself.

Can I write custom rules?

Yes. The JSON rule schema is fully documented in docs/rules.md. Target identifiers cover URI, headers, body, JSON paths, and more. Contribute rule files to the rules/ directory.

How do I monitor caddy-waf effectiveness?

Configure metrics_endpoint and scrape the JSON document, or bridge to Prometheus/Grafana per docs/prometheus.md. Ship logs to ELK via Filebeat following docs/caddy-waf-elk.md.

Why AGPL-3.0 license?

The AGPL ensures network users receive source code rights, aligning with the project's commitment to transparent security tooling. Commercial use is permitted; distribution of modified network-facing versions requires source availability.


Conclusion

Legacy WAFs force a brutal choice: comprehensive protection or operational sanity. caddy-waf refuses that trade-off. By embedding directly into Caddy's middleware chain, it delivers enterprise-grade defense — regex rule engines with anomaly scoring, multi-layer blacklists, intelligent rate limiting, geographic controls, and Tor awareness — through configuration that reads like modern infrastructure code, not security incantations.

The engineering decisions reveal serious craftsmanship: linear-time regex guarantees, wait-free concurrency primitives, bounded memory usage, hot reload without restarts, and graceful degradation when dependencies fail. This isn't a wrapper around someone else's engine. It's purpose-built security infrastructure.

If you're still wrestling with ModSecurity's XML rule syntax, paying cloud WAF rent for data you don't control, or leaving your APIs exposed because "proper security is too complex," the path forward is clear. The repository is active, documented, and waiting for your next deployment.

Stop wrestling. Start protecting. Head to https://github.com/fabriziosalmi/caddy-waf, run that one-liner install, and experience what modern WAF engineering actually feels like. Your 3 AM self will thank you.


Found this guide valuable? Star the repository, contribute rules from your threat landscape, and share your deployment stories. The future of web application security is open, modular, and finally — sane.

Advertisement

Comments (0)

No comments yet. Be the first to share your thoughts!

Leave a Comment

Apps & Tools Open Source

Apps & Tools Open Source

Bright Coding Prompt

Bright Coding Prompt

Categories

Advertisement
Advertisement