Docker containers running OWASP Coraza WAF with the OWASP Core Rule Set (CRS).
Three web server variants are available:
| Variant | Base | Image Tag |
|---|---|---|
| Caddy | Alpine | caddy-alpine |
| nginx | Debian (nginx-unprivileged) | nginx |
| Apache | Debian (httpd) | apache |
The containers act as a reverse proxy, inspecting traffic with Coraza WAF and CRS before forwarding to a backend service.
Stable tags include the CRS version and a timestamp: <CRS version>-<variant>-<date>.
Examples:
4-caddy-alpine-2025090510094.25-nginx-2025090510094.25.0-apache-202509051009
Rolling tags are updated on every release and always point to the latest build. They should not be used in production.
Examples:
caddy-alpinenginxapache
LTS (Long-Term Support) tags point to a designated LTS release and are updated less frequently than stable tags.
The LTS tag format is <CRS version>-<variant>-lts.
Examples:
4.25-caddy-alpine-lts4.25.0-nginx-lts4.25-apache-lts
Using docker compose:
# Caddy (port 8080)
docker compose --profile caddy up
# nginx (port 8081)
docker compose --profile nginx up
# Apache (port 8082)
docker compose --profile apache up
# All three
docker compose --profile all upOr run directly:
# nginx (non-root, listens on 8080)
docker run -d -p 8080:8080 -e BACKEND=myapp:8080 ghcr.io/coreruleset/coraza-crs:nginx
# Apache
docker run -d -p 80:80 -e BACKEND=myapp:8080 ghcr.io/coreruleset/coraza-crs:apache
# Caddy
docker run -d -p 8080:8080 -e BACKEND=myapp:8080 ghcr.io/coreruleset/coraza-crs:caddy-alpineTest that the WAF is working (adjust port to match the variant: 8080 for Caddy, 8081 for nginx, 8082 for Apache):
# Normal request — should return 200
curl http://localhost:8081/
# SQL injection — should return 403
curl "http://localhost:8081/?id=1%20AND%201=1"| Variable | Default | Description |
|---|---|---|
| CORAZA_RULE_ENGINE | On |
Enable Coraza. Accepted: On, Off, DetectionOnly |
| CORAZA_REQ_BODY_ACCESS | "On" |
Allow Coraza to access request bodies |
| CORAZA_REQ_BODY_LIMIT | 13107200 |
Maximum request body size for buffering |
| CORAZA_REQ_BODY_NOFILES_LIMIT | 524288 |
Maximum request body size excluding files |
| CORAZA_REQ_BODY_LIMIT_ACTION | "Reject" |
Action on body limit: Reject, ProcessPartial |
| CORAZA_REQ_BODY_JSON_DEPTH_LIMIT | 1024 |
Maximum JSON depth |
| CORAZA_ARGUMENTS_LIMIT | 1000 |
Maximum number of arguments |
| CORAZA_RESP_BODY_ACCESS | "On" |
Allow Coraza to access response bodies |
| CORAZA_RESP_BODY_LIMIT | 1048576 |
Maximum response body size for buffering |
| CORAZA_RESP_BODY_LIMIT_ACTION | "ProcessPartial" |
Action on response body limit |
| CORAZA_RESP_BODY_MIMETYPE | "text/plain text/html text/xml" |
MIME types to inspect |
| CORAZA_AUDIT_ENGINE | "RelevantOnly" |
Audit engine mode |
| CORAZA_AUDIT_LOG | /dev/stdout |
Audit log destination |
| CORAZA_AUDIT_LOG_FORMAT | JSON |
Audit log format: JSON, Native |
| CORAZA_AUDIT_LOG_PARTS | 'ABIJDEFHZ' |
Which transaction parts to log |
| CORAZA_AUDIT_LOG_RELEVANT_STATUS | "^(?:5|4[0-9][0-35-9])" |
HTTP status codes relevant for logging |
| CORAZA_AUDIT_LOG_TYPE | Serial |
Audit log type |
| CORAZA_AUDIT_STORAGE_DIR | /var/log/coraza/audit/ |
Audit log storage directory |
| CORAZA_DATA_DIR | /tmp/coraza/data |
Persistent data directory |
| CORAZA_TMP_DIR | /tmp/coraza |
Temporary files directory |
| CORAZA_DEBUG_LOG | /dev/null |
Debug log file |
| CORAZA_DEBUG_LOGLEVEL | 1 |
Debug log level (1-9) |
| Variable | Default | Description |
|---|---|---|
| PARANOIA | 1 |
CRS Paranoia Level for logging |
| ANOMALY_INBOUND | 5 |
Inbound anomaly score threshold |
| ANOMALY_OUTBOUND | 4 |
Outbound anomaly score threshold |
| BLOCKING_PARANOIA | 1 |
CRS Paranoia Level for blocking |
These variables apply to all variants (nginx, Apache, Caddy). Not every setting translates perfectly across servers — when a variable doesn't map to something meaningful, it's ignored on that variant.
| Variable | Default | Description | nginx | Apache | Caddy |
|---|---|---|---|---|---|
BACKEND |
localhost:80 |
Backend host:port to proxy to |
✅ | ✅ | ✅ |
PORT |
8080 |
Listen port | ✅ | ✅ | ✅ |
SERVER_NAME |
localhost |
Server name | ✅ | ✅ | — |
SERVER_TOKENS |
off (nginx/caddy), Prod (apache) |
Hide server version info | ✅ | ✅ | — |
WORKER_CONNECTIONS |
1024 |
Max concurrent connections per worker | ✅ | ✅ | — |
KEEPALIVE_TIMEOUT |
60s |
Client keepalive timeout | ✅ | ✅ | ✅ |
PROXY_TIMEOUT |
60s |
Backend proxy timeout | ✅ | ✅ | ✅ |
LOGLEVEL |
warn |
Server error log level | ✅ | ✅ | ✅ |
ACCESSLOG |
/dev/stdout |
Access log destination | ✅ | ✅ | ✅ |
ERRORLOG |
/dev/stderr |
Error log destination | ✅ | ✅ | — |
SSL_PORT |
8443 |
HTTPS listen port | ✅ | ✅ | — |
SSL_CERT_FILE |
(auto-generated) | TLS certificate path | ✅ | ✅ | — |
SSL_CERT_KEY_FILE |
(auto-generated) | TLS private key path | ✅ | ✅ | — |
SSL_PROTOCOLS |
nginx: TLSv1.2 TLSv1.3, apache: all -SSLv3 -TLSv1 -TLSv1.1 |
Allowed TLS protocols (syntax differs per server) | ✅ | ✅ | — |
SSL_CIPHERS |
Mozilla intermediate | TLS cipher suites | ✅ | ✅ | — |
SSL_PREFER_CIPHERS |
off |
Prefer server ciphers | ✅ | — | — |
SSL_DH_BITS |
2048 |
DH parameter size (2048/4096) | ✅ | — | — |
SSL_OCSP_STAPLING |
off |
OCSP stapling | ✅ | — | — |
SSL_VERIFY |
off |
Client certificate verification | ✅ | — | — |
SSL_VERIFY_DEPTH |
1 |
Client cert chain depth | ✅ | — | — |
ALWAYS_TLS_REDIRECT |
off |
Redirect HTTP to HTTPS | ✅ | ✅ | — |
HTTP2 |
on |
Enable HTTP/2 | ✅ | — | — |
H2_PROTOCOLS |
h2 h2c http/1.1 |
HTTP/2 protocols | — | ✅ | — |
SET_REAL_IP_FROM |
127.0.0.1 |
Trusted proxy for real IP | ✅ | — | — |
REAL_IP_HEADER |
X-Real-IP |
Header containing real client IP | ✅ | — | — |
REAL_IP_RECURSIVE |
on |
Recursive real IP lookup | ✅ | — | — |
REMOTEIP_HEADER |
X-Forwarded-For |
Header containing real client IP | — | ✅ | — |
REMOTEIP_INT_PROXY |
10.1.0.0/16 |
Trusted internal proxy range | — | ✅ | — |
A self-signed certificate is generated automatically at startup if no certificate is mounted. To use your own certificate, mount it at the SSL_CERT_FILE and SSL_CERT_KEY_FILE paths. Caddy handles TLS automatically via its built-in ACME support.
| Variable | Default | Description |
|---|---|---|
CORAZA_TAG |
coraza |
No longer applied. See below. |
CORAZA_TAG used to be interpolated into caddy's
CORAZA_DEFAULT_PHASE1_ACTION/CORAZA_DEFAULT_PHASE2_ACTION, which put
tag:'coraza' on every phase 1 and phase 2 CRS rule; nginx and Apache never
used it. Recent Coraza rejects metadata actions in SecDefaultAction
(SecDefaultAction must not contain metadata actions), so the tag had to go
and the variable is now inert on all three variants. To tag rules, set the
tag action on the rules themselves — for example with an override in
/opt/coraza/overrides — or filter logs on the rule ids instead.
- Caddy runs as a non-root user by default. The
cap_net_bind_servicecapability is added to allow binding on ports < 1024. Default port is 8080. - nginx uses
nginxinc/nginx-unprivileged(Debian-based, non-root). Apache useshttpd(Debian-based). Both build libcoraza and connector modules from source. Platforms:linux/amd64andlinux/arm64. - The audit log defaults to
/dev/stdout(visible viadocker logs).
The following configuration paths are shared across all variants:
| Path | Description |
|---|---|
/opt/coraza/config/coraza.conf |
Main Coraza WAF configuration |
/opt/coraza/config/coraza-rules.conf |
Rule include chain (nginx/Apache) |
/opt/coraza/config/crs-setup.conf |
OWASP CRS configuration |
/opt/coraza/config.d/*.conf |
User configuration files |
/opt/coraza/owasp-crs/rules/*.conf |
OWASP CRS rule files |
/opt/coraza/owasp-crs/plugins/*.conf |
CRS plugins |
/opt/coraza/plugins/*.conf |
User plugins |
/opt/coraza/rules/*.conf |
Additional built-in rules |
/opt/coraza/rules.d/*.conf |
User defined rule sets |
/opt/coraza/overrides/*.conf |
Rule/configuration overrides |
| Variable | Default | Description |
|---|---|---|
CRS_VERSION |
4.25.2 |
OWASP CRS release version (LTS) |
CADDY_VERSION |
2.11.6 |
Caddy Docker tag (caddy variant) |
NGINX_VERSION |
1.30.6 |
nginx image version (nginx variant) |
HTTPD_VERSION |
2.4 |
httpd image version (apache variant) |
LIBCORAZA_VERSION |
v1.8.0 |
libcoraza release (nginx/apache variants) |
CORAZA_CADDY_VERSION |
v2.6.2 |
coraza-caddy release (caddy variant) |
CORAZA_NGINX_VERSION |
0.23.0 |
coraza-nginx release (nginx variant) |
CORAZA_APACHE_VERSION |
0.22.0 |
coraza-apache release (apache variant) |
We require buildx >= v0.9.1. See official documentation.
# See all build targets
docker buildx bake -f ./docker-bake.hcl --print
# Build all targets
docker buildx create --use --platform linux/amd64,linux/arm64
docker buildx bake -f docker-bake.hcl
# Build a single target
docker buildx bake -f docker-bake.hcl --set "*.platform=linux/amd64" nginxtests/run-tests.sh starts the image under test as a reverse proxy in front of a
traefik/whoami backend and drives it with curl: legitimate traffic must be
proxied through, attack payloads must be blocked by CRS, and the documented
environment variables must actually change that behaviour. It is the same script
the Verify Images workflow runs on every pull request.
# Build the caddy image and test it
./tests/run-tests.sh -v caddy -b
# Test an image that is already available locally
./tests/run-tests.sh -v nginx -i ghcr.io/coreruleset/coraza-crs:nginxOptions:
| Option | Description |
|---|---|
-v <variant> |
Variant to test: caddy, nginx or apache (default: caddy). Bake target names such as nginx-lts are accepted too. |
-i <image> |
Image to test (default: coraza-crs-test:<variant>). |
-b |
Build the image with docker buildx bake before running the tests. |
The script requires docker and curl, publishes containers on ephemeral
localhost ports, and removes everything it started on exit. Container logs are
dumped when a test fails. Every check runs; the script exits non-zero if any of
them failed.
What is covered:
- The container starts and keeps running.
- Legitimate requests (query string, urlencoded body, JSON body) are proxied to the backend and answered with the backend's response.
- Attacks are blocked with
403: XSS and SQL injection in the query string, path traversal, RCE attempt, scanner user agent, SQL injection in a POST body, XSS in a JSON body. A disallowed HTTP method is rejected too (403from CRS, or405when the web server answers it first). CORAZA_RULE_ENGINE=DetectionOnlyandCORAZA_RULE_ENGINE=Offlet attacks through.ANOMALY_INBOUNDis applied to the rule set byactivate-rules.sh.- Custom rules mounted into
/opt/coraza/rules.dare loaded and enforced, including a phase 4 rule onRESPONSE_BODY— a guard against response body inspection silently not running. - Audit logging: default log directories, serial audit log written on attack,
concurrent audit log files written on attack, and creation of a custom
CORAZA_AUDIT_STORAGE_DIRby the entrypoint.
Add Coraza configuration without overwriting defaults by mounting *.conf files into:
/opt/coraza/config.d— additional configuration/opt/coraza/rules.d— additional rule sets
Download and mount plugins to /opt/coraza/plugins:
curl -sSL https://github.com/coreruleset/wordpress-rule-exclusions-plugin/archive/refs/tags/v1.0.0.tar.gz -o wordpress.tar.gz
tar xvf wordpress.tar.gz --strip-components 1 'wordpress-rule-exclusions-plugin*/plugins'
docker run -v $(pwd)/plugins:/opt/coraza/plugins ghcr.io/coreruleset/coraza-crs:nginxTo use your own web server configuration, mount it at:
- Caddy:
/config/caddy/Caddyfile - nginx:
/config/nginx/nginx.conf - Apache:
/config/apache/coraza.conf
To replace the Coraza configuration entirely, mount at /config/coraza/coraza.conf.