Hardened Caddy web server distribution with Coraza WAF and OWASP CRS v4 - tested with 122 integration cases, signed supply chain (Cosign/SLSA), and safe DetectionOnly staged rollout.
Curated by Miguel Lozano โข GitHub โข Container Registry
- Overview
- Quick Start
- Architecture
- Configuration Guide
- Docker Deployment
- Testing & Validation
- Monitoring & Observability
- Operations Documentation
- Security
- Changelog
- Contributing
- License
- Contact & Support
Problem: Deploying a web application firewall typically requires weeks of tuning, dedicated appliances, and specialized security expertise. Most WAF solutions block legitimate traffic on day one, disrupting your users and forcing you to disable protections you just deployed.
This project solves that. It packages Caddy - the web server with automatic TLS - with Coraza WAF and the OWASP Core Rule Set into a single hardened container. The WAF defaults to DetectionOnly mode, giving you a safe observation window before enforcement. It comes preconfigured with OWASP CRS v4.29.0 covering SQL injection, XSS, command injection, and OWASP Top 10 vectors across 12 rule families - allowing you to baseline legitimate application traffic and tune exclusions during observation before enforcing active blocking.
Starting from v3.0.0, the image ships with updated Caddy 2.11.4, official upstream rate limiting and DNS plugins, and native security header support - giving the project full control over maintenance cadence, security patches, and feature development.
- Non-root execution: Runs as
caddyuser (UID 1337) - no root privileges - Supply chain security: Pinned versions, SHA256 verification of OWASP CRS rules, Cosign-signed images, SBOM attestations
- Multi-stage builds: Minimal attack surface, optimized layers
- Health monitoring: Process verification healthcheck at both image and compose level
- Structured logging: JSON logs for SIEM integration
- Coraza WAF v2.6.1: Modern, high-performance web application firewall engine
- OWASP CRS v4.29.0: Core Rule Set covering SQLi, XSS, RCE, and protocol violations across 12 rule families
- DetectionOnly by default: Mitigates false-positive operational risk during initial rollout
- Audit logging: JSON audit logs to stdout for easy monitoring
- Rate limiting: Built-in rate limiting via
mholt/caddy-ratelimit - Security headers: Automated security header injection via Caddy's native
headerdirective
- Optimized Alpine base: Small footprint (~45MB compressed)
- TLS by default: Automatic Let's Encrypt integration
- Multi-architecture: Supports linux/amd64 and linux/arm64
- Cloud-native: Perfect for Kubernetes, Docker Swarm, and standalone Docker
- Bare-metal ready: Systemd service file included for non-containerized deployments
- Docker 24.x+ and Docker Compose v2.x+
docker pull ghcr.io/developmi/caddy-waf:v3.5.5cp .env.example .env
# Edit .env with your domain/backend/image valuescp Caddyfile.example Caddyfile
# Edit Caddyfile for your domain and upstreamsdocker build -t your-registry/your-caddy-waf:custom \
--build-arg CORAZA_CADDY_REF=v2.6.1 \
--build-arg CADDY_RATELIMIT_REF=5625512 \
--build-arg CADDY_DNS_CLOUDFLARE_REF=v0.2.4 \
.Then set CADDY_WAF_IMAGE=your-registry/your-caddy-waf:custom in .env.
{
order coraza_waf first
}
yourdomain.com {
respond "Caddy with Coraza WAF is running" 200
}docker compose up -dcaddy-waf/
โโโ assets/ # Brand assets (logo)
โโโ deploy/
โ โโโ systemd/ # Systemd service unit for bare-metal
โโโ docs/ # Operations docs (deployment, IR, compliance)
โโโ .github/workflows/ # CI/CD (build, scan, sign, push)
โโโ metrics/ # Backend-agnostic Prometheus scrape config
โโโ grafana/ # Provisioned datasources + dashboards
โโโ Dockerfile # Multi-stage build with pinned plugins
โโโ docker-compose.yml # Production-grade compose with security hardening
โโโ Caddyfile # Runtime configuration - untracked, generated from Caddyfile.example (WAF + TLS + reverse proxy)
โโโ Caddyfile.example # Templated configuration with 5 deployment examples
โโโ .env.example # Environment variable template (3 groups)
โโโ TUNING.md # WAF tuning guide per application type
โโโ ROADMAP.md # Planned enhancements and compliance roadmap
โโโ CHANGELOG.md # Version history (Keep a Changelog)
โโโ CONTRIBUTING.md # Contribution guidelines
โโโ SECURITY.md # Vulnerability disclosure policy
โโโ LICENSE # MIT License
flowchart LR
Client[Client] -->|HTTPS :443| Caddy[Caddy v2.11.4]
Caddy -->|WAF layer| Coraza[Coraza WAF v2.6.1]
Coraza -->|OWASP CRS v4.29.0| Rules[CRS v4 Rules]
Coraza -->|Decision| Action{Allow?}
Action -->|Yes| Backend[Upstream Backend]
Action -->|No| Block[Block + Audit Log]
Block -->|JSON| SIEM[SIEM / Log Aggregator]
Caddy -->|Auto TLS| LE[Let's Encrypt]
The WAF operates in three modes (configured in Caddyfile):
- DetectionOnly (Default): Logs attacks without blocking - perfect for initial deployment
- On: Active protection - blocks malicious requests
- Off: Disables WAF completely
Recommended rollout: Keep
SecRuleEngine DetectionOnlyfor a 7โ14 day observation window. Review audit logs, tune CRS exclusions, then switch toSecRuleEngine Ononly after establishing a stable false-positive baseline.
{
email [email protected]
order coraza_waf first
# JSON logging for observability
log {
output stdout
format json
}
}
(waf) {
coraza_waf {
directives `
Include /etc/caddy/coraza.conf
Include /etc/caddy/owasp-crs/crs-setup.conf
Include /etc/caddy/owasp-crs/rules/*.conf
# Start with DetectionOnly, change to On after tuning
SecRuleEngine DetectionOnly
# Audit logging
SecAuditEngine RelevantOnly
SecAuditLog /dev/stdout
SecAuditLogFormat JSON
`
}
}
# Your site configuration
example.com {
import waf
reverse_proxy backend:8080
}For detailed WAF tuning, rule exceptions, and performance optimization, see the complete TUNING GUIDE.
Project roadmap and planned security integrations are tracked in ROADMAP.md.
Mount your custom rules directory:
volumes:
- ./custom-crs:/etc/caddy/owasp-crs| Variable | Default | Description |
|---|---|---|
CADDY_WAF_IMAGE |
ghcr.io/developmi/caddy-waf:v3.5.5 |
Caddy WAF image reference |
EXAMPLE_APP_IMAGE |
containous/whoami:latest |
Demo backend image |
SITE_ADDRESS |
localhost |
Site address/server name used by Caddy |
BACKEND_UPSTREAM |
example-app:80 |
Reverse proxy backend upstream |
ACME_EMAIL |
(empty) | Email for Let's Encrypt certificates |
github.com/corazawaf/coraza-caddy/[email protected]- Coraza WAF integrationgithub.com/mholt/[email protected]- Rate limiting (DDoS protection)
Note:
caddy-ratelimitdevelopment is active but releases are not tagged beyondv0.1.0. This image pins the module by commit SHA (5625512) to include post-tag fixes. See Dockerfile for the pinned reference.
github.com/caddy-dns/[email protected]- Cloudflare DNS for ACME challenges
Security headers are handled via Caddy's native
headerdirective - no external plugin needed.
# Start with example backend
cp .env.example .env
cp Caddyfile.example Caddyfile
docker compose up -ddocker build \
--build-arg CORAZA_CADDY_REF=v2.6.1 \
--build-arg CADDY_RATELIMIT_REF=5625512 \
--build-arg CADDY_DNS_CLOUDFLARE_REF=v0.2.4 \
-t caddy-waf:custom .The unit runs on the host network namespace, so it uses the zero-trust
config deploy/systemd/Caddyfile.systemd - the admin API is bound to loopback
only (admin localhost:2019). Never use 0.0.0.0:2019 on a host network: the
admin API accepts config POSTs.
Prerequisite: caddy binary with Coraza and /etc/caddy/coraza.conf + /etc/caddy/owasp-crs installed on the host (see pre-flight checklist in docs/deployment-checklist.md).
sudo cp deploy/systemd/caddy-waf.service /etc/systemd/system/
sudo useradd -r -s /usr/sbin/nologin caddy-waf
sudo install -D -o caddy-waf -g caddy-waf -m 0640 deploy/systemd/Caddyfile.systemd /etc/caddy/Caddyfile
sudo -u caddy-waf caddy validate --config /etc/caddy/Caddyfile
sudo systemctl daemon-reload
sudo systemctl enable --now caddy-wafVerify the image signature before pulling in production:
cosign verify \
--certificate-identity "https://github.com/developmi/caddy-waf/.github/workflows/docker-build-scan-sign.yml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
ghcr.io/developmi/caddy-waf@sha256:<digest>Tip: Use immutable image digests (
@sha256:...) instead of version tags in production for deterministic deployments.
# Check container health
docker ps --filter "name=caddy-waf"
# View logs
docker logs caddy-waf
# Test WAF is working
curl -I https://yourdomain.com# Scan image with Trivy
docker run --rm aquasec/trivy image ghcr.io/developmi/caddy-waf:v3.5.5
# Scan with Docker Scout
docker scout quickview ghcr.io/developmi/caddy-waf:v3.5.5The WAF and perimeter defenses are rigorously verified using the go-ftw framework and automated HTTP assertion gates across 122 automated integration tests structured under Screaming Architecture:
# General test suite (runs all linters + 122 WAF tests + live security headers + bare-boot regression)
make test
# Granular suites (run in milliseconds for fast feedback loops)
make test-waf # Runs all 122 integration tests + live security header verification
make test-baseline # Runs 00-baseline (12 tests: clean traffic + false-positive resilience)
make test-evasion # Runs 01-evasion (6 tests: double-encoding, fullwidth, bypass vectors)
make test-hardening # Runs 02-hardening (10 tests: .env, .git, .aws, Docker, .sql, .bak + headers)
make test-crs # Runs crs/ (94 tests across all 12 OWASP CRS v4.29.0 rule families)
make test-suite SUITE=crs/942-attack-sqli # Target any specific family or subdirectory
make test-boot # Runs bare-boot regression gate (proves baked container boots as UID 1337)| Suite Domain | Tests | Focus & Attack Vectors Covered |
|---|---|---|
00-baseline |
12 | Container health, clean GET/POST, and false-positive immunity against complex JSON, GraphQL, UTF-8 Spanish accents, Markdown, JWT Bearer tokens, and UUIDs. |
01-evasion |
6 | Anti-evasion against double URL encoding, fullwidth unicode scripts, header-based payload splitting, and body obfuscation. |
02-hardening |
10 | Perimeter defense-in-depth: native blocking of .env, .git, .aws/credentials, .docker/config.json, .htpasswd, .sql dumps, .bak, and .conf (active even in DetectionOnly mode). Automated verification of Server and X-Powered-By banner suppression + required security headers (Permissions-Policy, HSTS, CSP/Cross-Domain). |
crs/911-method |
4 | HTTP method enforcement (blocks TRACE, CONNECT, WebDAV arbitrary methods). |
crs/913-scanner |
6 | Security scanner detection (Nikto, sqlmap, Havij, Acunetix, Arachni). |
crs/920-protocol |
6 | HTTP protocol enforcement (missing Host, missing Accept, Range anomalies, URL limits). |
crs/921-attack-proto |
4 | Protocol-level attacks (HTTP Request Smuggling, response splitting, CRLF). |
crs/922-multipart |
4 | Multipart form/file upload validation (boundary evasion, header corruption). |
crs/930-attack-lfi |
8 | Local File Inclusion (dot-dot-slash traversal, absolute path probes, OS files). |
crs/931-attack-rfi |
4 | Remote File Inclusion (PHP wrappers, data://, SMB UNC paths, off-site inclusions). |
crs/932-attack-rce |
10 | Remote Code Execution & Unix/Windows shell injection (pipes, command chaining, backticks). |
crs/933-attack-php |
6 | PHP injection attacks (php://input, system(), eval(), open tags). |
crs/934-attack-generic |
10 | Application generic attacks: Cloud metadata SSRF (AWS IMDSv1, GCP, Kube API), Prototype Pollution (__proto__, constructor.prototype), SSTI ({{...}}), and Node.js/JS execution (require('child_process'), eval(), fs.readFileSync(), ReDoS). |
crs/941-attack-xss |
10 | Cross-Site Scripting (HTML tags, SVG vectors, event handlers, JavaScript URIs). |
crs/942-attack-sqli |
12 | SQL Injection (tautologies, UNION SELECT, blind boolean/sleep, comment syntax, MSSQL xp_cmdshell). |
crs/943-session-fix |
4 | Session fixation (session ID in URL parameters, Set-Cookie manipulation). |
crs/944-attack-java |
6 | Java attacks (Log4Shell JNDI injection, Remote Method Invocation, serialized payloads). |
{
"level": "info",
"ts": 1678901234.567,
"logger": "http.log.access",
"msg": "handled request",
"request": {
"method": "GET",
"uri": "/test",
"proto": "HTTP/2",
"remote_ip": "192.168.1.100"
},
"waf_action": "detected",
"waf_rule_id": "941100"
}The Caddy admin endpoint (:2019/metrics) exposes real Prometheus metrics:
caddy_http_requests_total- Total requests (labels: server, handler, method)caddy_http_request_duration_seconds- Request latency histogram (labels includecode)caddy_http_requests_in_flight- Concurrent requests gaugecaddy_reverse_proxy_upstreams_healthy- Backend upstream health (0/1)caddy_config_last_reload_successful- Config reload status
Note: coraza-caddy (up to v2.6.1) does not export
coraza_waf_*metrics (upstream limitation tracked in coraza-caddy#82). WAF rule IDs and decisions are available in the JSON audit log on stdout, not on/metrics.
Dual-backend observability: the same metrics/prometheus.yml scrape config
works with both VictoriaMetrics and Prometheus (identical Prometheus
exposition format), so Caddy's /metrics output needs no changes. Pick one
profile:
# VictoriaMetrics + Grafana (Grafana on http://localhost:3000)
docker compose --profile observability-vm up -d
# Prometheus + Grafana (Grafana on http://localhost:3001)
docker compose --profile observability-prom up -dProfile services are not started by plain docker compose up - default
behavior is unchanged. Grafana is bound to loopback only (127.0.0.1), with
anonymous read access so the dashboard opens without login
(admin UI: admin / admin, override with GRAFANA_ADMIN_USER /
GRAFANA_ADMIN_PASSWORD).
metrics/prometheus.yml- single canonical scrape config, mounted read-only: VictoriaMetrics consumes it via-promscrape.config, Prometheus via--config.file. Targets the internalcaddy-waf:2019.- Caddyfile.example exposes the admin endpoint (
/metrics) on0.0.0.0:2019and enablescaddy_http_*metric collection. - Each profile provisions its own Grafana datasource (VictoriaMetrics or
Prometheus) pointing at the backend, and loads the same dashboard
(
grafana/dashboards/caddy-waf.json): request rate, status codes, p95 latency, in-flight requests, backend health, throughput, request errors, plus a WAF mode note.
Port 2019 (Caddy admin API) is never published to the host - the admin
API can accept config POSTs, so it stays strictly inside the internal Docker
network. Scraping happens over the caddy-network bridge only. Backends
(VictoriaMetrics:8428, Prometheus:9090) are also internal-only.
docker compose --profile observability-vm down -v
docker compose --profile observability-prom down -vSee TUNING.md for metric details.
Operational runbooks live in docs/:
| Document | Purpose |
|---|---|
| Deployment checklist | Step-by-step deploy, verify, and rollback for Docker Compose and systemd (bare-metal) |
| Incident response | Runbook for WAF false positives and generic incidents, with severity table |
| SOC 2 mappings | Trust Services Criteria mapped to implemented controls, with file:line evidence and honest gaps |
| SLSA compliance | Supply-chain level assessment of the build pipeline (current: L2, partial L3) |
This project follows a coordinated disclosure policy. If you discover a vulnerability, do not open a public issue. See SECURITY.md for:
- Supported versions
- Reporting instructions (GitHub Advisory + email)
- Response timelines (48h acknowledgment, 30-day fix target)
- Supply chain verification (Cosign + Trivy)
| Version | Supported |
|---|---|
| 3.5.x | โ Yes (current) |
| 2.0.x | โ No |
| 1.0.x | โ No |
Security advisories and resolved CVEs are documented in SECURITY.md.
See CHANGELOG.md for the full version history. The project follows Keep a Changelog and Semantic Versioning.
| Version | Date | Highlights |
|---|---|---|
| 3.5.5 | 2026-09-23 | 122-test integration matrix (12 CRS families), granular test runners, native Caddy perimeter hardening & banner suppression, CI test gate |
| 3.5.4 | 2026-09-21 | Coraza WAF 2.6.1, grpc v1.83.2, CVE-2026-84304 & CVE-2026-84445 resolved, empty .trivyignore |
| 3.5.2 | 2026-09-09 | coraza-caddy v2.6.0 (WebSocket+WAF fixes, grpc v1.82.1), digest-pinned base, CI/CD hardening |
| 3.4.0 | 2026-08-24 | OWASP CRS 4.29.0, Trivy v0.74.0, 20-case integration suite (OWASP Top 10 2025), Actions bumps |
| 3.3.2 | 2026-08-14 | WAF default active (DetectionOnly), dual-arch scanning, boot regression gate, systemd variant tracked |
| 3.3.1 | 2026-08-11 | HEALTHCHECK via admin /metrics (curl), resource limits (512m/1 CPU), JSON-file log rotation, version alignment |
| 3.3.0 | 2026-08-11 | Tool bumps (hadolint 2.15.1, go-ftw 2.5.0, Trivy v0.73.0), full apk upgrade, HEALTHCHECK JSON, version alignment |
| 3.0.0 | 2026-07-01 | Caddy 2.11.4 upgrade, Official upstream plugins, CVE fixes, security headers plugin |
| 2.0.0 | 2026-03-14 | Security hardening, systemd, OCI labels, CI updates |
| 1.0.0 | 2026-02-09 | Initial release with Coraza WAF + OWASP CRS |
Contributions are welcome. Please read CONTRIBUTING.md before opening a pull request. This project follows Conventional Commits and the Developmi engineering standard.
- Report a bug or request a feature: GitHub Issues
- Advanced configuration: TUNING.md
- Roadmap: ROADMAP.md
For commercial support, custom configurations, or security consulting:
- Website: developmi.com
- Email: [email protected]
- GitHub: developmi
Copyright ยฉ 2026 Miguel Lozano | Developmi. All rights reserved. Licensed under the MIT License.
- Caddy Server - Amazing web server with automatic HTTPS
- Coraza WAF - Open-source OWASP Coraza WAF engine
- OWASP Core Rule Set - Industry-standard protection rules
- Developmi - DevOps & Security consulting
Maintained by: Miguel Lozano | Developmi
- Role: Cloud & Infrastructure Engineer | FinOps & Bare Metal Specialist | AI Sovereignty Strategist under NIST/DORA Standards
- Philosophy: Security is not a feature; it is the baseline.
- Website: developmi.com
- GitHub: developmi
- LinkedIn: Miguel Lozano
ยฉ 2026 Miguel Lozano | Developmi. All rights reserved.