feat(waf): add Coraza SPOA CRS bundle - #164
Conversation
Add a pinned OWASP CRS 4.25.0 submodule and baseline Coraza SPOA configuration so the M1 stack can build a reproducible WAF image. Also fix the Compose/HAProxy wiring needed for the full-stack smoke path through Coraza. Made-with: Cursor
There was a problem hiding this comment.
Pull request overview
Adds a reproducible Coraza SPOA + OWASP CRS bundle and wires it into the full-stack Docker Compose setup so HAProxy can route requests through Coraza for request inspection.
Changes:
- Adds pinned OWASP CRS (as a git submodule) plus baseline Coraza config (
coraza.conf,crs-setup.conf,coraza-spoa.yaml). - Introduces a reproducible Coraza SPOA image build (
deploy/docker/coraza.Dockerfile) andmake coraza-build. - Updates HAProxy SPOE/SPOP wiring and Docker Compose mounts to route traffic through Coraza.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| deploy/docker/docker-compose.yml | Build Coraza image in-compose, adjust HAProxy config mounts, update Coraza healthcheck |
| deploy/docker/coraza/rules/.gitkeep | Removes no-longer-used placeholder rules directory |
| deploy/docker/coraza.Dockerfile | New Dockerfile to bake in Coraza config + CRS bundle |
| configs/haproxy/haproxy.cfg | Updates SPOE wiring (send-spoe-group), anomaly score var name, and SPOP checks |
| configs/haproxy/coraza.cfg | Updates SPOE agent configuration to use groups and adds spoe-group |
| configs/haproxy/README.md | Documents updated SPOE group semantics and anomaly score variable |
| configs/coraza/crs-setup.conf | Adds baseline CRS paranoia/anomaly thresholds for the pinned CRS version |
| configs/coraza/coraza.conf | Adds baseline Coraza directives and includes CRS setup + rules |
| configs/coraza/coraza-spoa.yaml | Adds Coraza SPOA service config used by the Docker image |
| configs/coraza/README.md | Documents pinned versions, defaults, and CRS update workflow |
| README.md | Updates “how to access” instructions and adds WAF smoke-test curl |
| README.commands.md | Adds make coraza-build to command reference |
| Makefile | Adds coraza-build target |
| .gitmodules | Adds CRS submodule definition |
| .dockerignore | Restricts Docker build context for reproducible Coraza image builds |
Comments suppressed due to low confidence (2)
configs/haproxy/haproxy.cfg:45
http-request send-spoe-groupis executed before theHostACL rejection, so even requests for unrecognised vhosts will be sent to Coraza. This increases WAF load and expands the DoS surface unnecessarily. Consider moving the send after thehost_appcheck or adding a condition (e.g., only send to SPOE whenhost_appmatches).
filter spoe engine coraza config /usr/local/etc/haproxy/coraza.cfg
http-request send-spoe-group coraza coraza-req
# Reference vhost: only requests for app.local are accepted.
# Anything else is rejected before it hits the backend.
acl host_app hdr(host) -i app.local app.local:80
http-request deny deny_status 421 if !host_app
configs/haproxy/coraza.cfg:18
- The comment example still refers to
txn.coraza.score, but the HAProxy config/README now usetxn.coraza.anomaly_score. Please update this example to avoid confusion when debugging.
# All variables produced by the agent are exposed under
# txn.coraza.* (e.g. txn.coraza.action, txn.coraza.score).
option var-prefix coraza
| healthcheck: | ||
| test: ["CMD", "/coraza-spoa", "--help"] | ||
| test: ["CMD", "/bin/busybox", "nc", "-z", "127.0.0.1", "9000"] | ||
| interval: 10s | ||
| timeout: 5s | ||
| retries: 10 |
There was a problem hiding this comment.
Since coraza now has a real port-based healthcheck, HAProxy should depend on coraza being healthy (not just started). Otherwise HAProxy may start accepting traffic before the SPOA is listening, causing SPOE errors / fail-open behavior during startup. Update the haproxy.depends_on.coraza.condition to service_healthy to match the other services.
There was a problem hiding this comment.
Done in 372427a — changed condition: service_started to condition: service_healthy for the coraza dependency under haproxy. HAProxy will now wait until the Coraza SPOA passes its netcat healthcheck before starting, preventing SPOE fail-open during startup.
| ## Pinned versions | ||
|
|
||
| - Coraza SPOA image: `ghcr.io/corazawaf/coraza-spoa:0.6.1` | ||
| - OWASP Core Rule Set: `v4.25.0`, pinned as the `configs/coraza/crs` git | ||
| submodule | ||
|
|
There was a problem hiding this comment.
Because CRS is a git submodule, first-time users who clone without --recurse-submodules (or CI checkouts that don’t init submodules) will end up with an empty configs/coraza/crs/ and a non-functional WAF ruleset. Add a short “clone/init” snippet (e.g., git submodule update --init --recursive) to make the setup reproducible.
There was a problem hiding this comment.
Done in 565dfa3 — added a "First-time setup" section to configs/coraza/README.md with the git submodule update --init --recursive snippet, placed prominently before the "Pinned versions" section.
…proxy Agent-Logs-Url: https://github.com/bihius/guard-proxy/sessions/b622cbfb-f3d0-4b11-bfde-699a0e006a89 Co-authored-by: bihius <119067436+bihius@users.noreply.github.com>
Agent-Logs-Url: https://github.com/bihius/guard-proxy/sessions/8f2c8162-4b45-4683-9d81-4d2ebc12447e Co-authored-by: bihius <119067436+bihius@users.noreply.github.com>
- add alembic upgrade head before starting Uvicorn - add Postgres DATABASE_URL to Docker .env.example - update related Docker command docs Made-with: Cursor
Summary
configs/coraza/crswith baseline Coraza SPOA configuration.deploy/docker/coraza.Dockerfileandmake coraza-buildfor a reproducible Coraza SPOA image.Test plan
make coraza-builddocker-compose -f deploy/docker/docker-compose.yml --env-file deploy/docker/.env up -d --builddocker-compose -f deploy/docker/docker-compose.yml --env-file deploy/docker/.env psshowed all five services healthycurl -i -H 'Host: app.local' http://localhost:8080/healthreturned200 OKcurl -i -H 'Host: app.local' "http://localhost:8080/?id=1%27%20OR%20%271%27=%271"returned403 Forbiddendocker-compose -f deploy/docker/docker-compose.yml --env-file deploy/docker/.env down -vuv run pytest --cov=appuv run mypy app/uv run ruff check app/pnpm run type-checkpnpm run lintdocker run --rm -v "$PWD/configs/haproxy:/usr/local/etc/haproxy:ro" haproxy:3.0-alpine haproxy -c -f /usr/local/etc/haproxy/haproxy.cfgCloses #106
Related to #107