Overview
Coraza is an open-source web application firewall that ships with the OWASP Core Rule Set (CRS). This template deploys the coraza-crs image, which runs Coraza as a plugin inside the Caddy web server, as a reverse proxy in front of a workload you already run. Traffic enters the WAF, is inspected against CRS and any rules you add, and is forwarded to the workload behind it. The template does not deploy the workload it protects, and it only inspects traffic that is actually routed through it — see Important Notes.Architecture
- WAF Workload — Coraza and CRS on Caddy, listening on
WAFPortand proxying totargetWorkload:targetPort. Astandardworkload that autoscales on CPU between one and three replicas. - Startup Hook — A
postStarthook that points the image’s Caddy reverse-proxy handler at your target workload. It resolves the handler by name, waits for the Caddy admin API with a bounded timeout, reads the result back, and fails closed: a WAF that cannot configure itself refuses to serve rather than passing traffic through uninspected. - Custom Rules — Your own seclang rules, loaded after the Core Rule Set.
What Gets Created
- Standard Workload — (
RELEASE_NAME-coraza-waf): the Coraza container, listening onWAFPort, publicly reachable and reachable from the same GVC. - Startup Secret — (
RELEASE_NAME-coraza-startup): an opaque secret holding the startup hook script, mounted into the container. - Custom Rules Secret — (
RELEASE_NAME-coraza-custom-rules): an opaque secret holding your rules, mounted at/opt/coraza/rules.d/custom.conf. It ships with one example rule. - Identity & Policy — An identity for the WAF workload with
revealscoped to exactly those two secrets. No cloud bindings are attached.
This template does not create a GVC. You must deploy it into an existing GVC alongside the workload you want to protect.
Prerequisites
The workload you intend to protect must already exist, and the WAF must be able to reach it:- Set
targetWorkloadto that workload’s fully qualified internal address,WORKLOAD_NAME.GVC_NAME.cpln.local, andtargetPortto the port it serves. - Set that workload’s internal firewall to
same-gvc,same-org, or aworkload-listthat includesRELEASE_NAME-coraza-waf. The internal default isnone, which blocks the WAF from reaching it.
Installation
To install, follow the instructions for your preferred method:UI
Browse, install, and manage templates visually
CLI
Manage templates from your terminal
Terraform
Declare templates in your Terraform configurations
Pulumi
Declare templates in your Pulumi programs
Configuration
The defaultvalues.yaml for this template:
Proxy Target
targetWorkload— The internal DNS name of the workload to proxy traffic to, in the formWORKLOAD_NAME.GVC_NAME.cpln.local. This must be changed before deploying. The short workload name alone is not reliable; always use the fully qualified form.targetPort— The port the target workload serves on.WAFPort— The port the WAF workload exposes, and the port clients connect to (default80).
WAFPort change for the new container port to propagate at the edge.
Choosing an Image Tag
Only the*-caddy-alpine-* variants work. The -nginx- and -apache- builds of coraza-crs ship no Caddy binary, so this template cannot configure them. Upstream publishes a *-caddy-alpine-* build alongside them for each CRS release — take the newest of those.
Pin a digest or a datecode, never a moving tag. Tags such as 4.25-caddy-alpine-lts and caddy-alpine are repointed by upstream, so the image can change under a deployment nobody touched. A digest (coraza-crs@sha256:…) or a datecode (4.28-caddy-alpine-202608260808) names one specific build. The shipped default is a pinned digest of a specific build.
New CRS releases are therefore never picked up on their own — updating the rule set is a deliberate image change.
Request Body Size and Timeout
Bodies larger than Coraza’s in-memory limit are buffered to disk under/tmp/coraza and inspected in full — there is nothing to enable. The startup hook creates that directory unconditionally, which matters if you point image at an older coraza-crs build: those do not create it themselves, and without it every large request fails with a 500 and failed to append request body: … no such file or directory, which reads as a WAF fault rather than a missing directory.
Coraza inspects request bodies on the CPU, and timeoutSeconds cuts a request off part-way through. Together, resources.cpu and timeoutSeconds set the largest request body the WAF accepts:
cpu: 500m, timeoutSeconds: 30) that is about 1.8 MB, measured against the public endpoint:
A body too large to inspect within
timeoutSeconds returns a 504 from the WAF, not from your application. GET traffic is unaffected, so a smoke test that only issues GETs will not reveal the ceiling.
Inspection cost scales with CPU, but not linearly: the measured rate is about 17.5 ms per KB of body at cpu: 500m, and below roughly 250m CPU quota throttling makes it disproportionately worse. To accept larger bodies, raise resources.cpu first — that inspects the same body faster, rather than merely waiting longer — then raise timeoutSeconds, and raise resources.memory alongside them, since memory use scales with body size (a single inspected 3 MB body peaked at about 124 MiB).
Two ceilings you cannot raise from values: Coraza stops inspecting bodies above 12.5 MiB (SecRequestBodyLimit, fixed in the image), and timeoutSeconds also caps how long your own upstream has to answer — so set it above your application’s slowest response.
timeoutSeconds is enforced at the public edge. A caller inside the GVC that connects to the WAF directly over cpln.local is not subject to it, so a body-size or latency test run from inside the GVC will not reproduce the 504.Resources and Placement
resources.cpu/resources.memory— CPU and memory for the WAF container. These are the limits, and they size request-body inspection — see Request Body Size and Timeout.multiZone— Whentrue, deploys replicas across multiple zones for higher availability.
Not all locations support multi-zone deployments. Confirm that your target location supports multi-zone before enabling this option.
Custom Rules
Add your own rules by editing the secret namedRELEASE_NAME-coraza-custom-rules, either in the Console or with cpln secret edit RELEASE_NAME-coraza-custom-rules. It ships with one example rule, which blocks any request whose URI contains attack:
920450 blocks any request carrying an Expect: 100-continue header, which some HTTP clients — curl among them — add automatically for large uploads. An application behind the WAF can therefore see 403s that have nothing to do with its payload. Removing that one rule leaves the rest of CRS in force, including the example rule above.
Rules are read at startup. After changing the secret, restart the replicas with
cpln workload force-redeployment RELEASE_NAME-coraza-waf --gvc GVC_NAME for the change to take effect.Logging
Coraza’s access, audit, and debug logs go to/dev/stdout and are readable through the built-in logging interface:
[INFO] and [FATAL] lines appear in the same stream. Log destinations and verbosity can be changed through the CORAZA_* environment variables on the workload after installation.
Connecting
Send traffic to the WAF, not to the workload behind it.
Important Notes
- This only protects traffic that goes through it. The WAF is a proxy, so the workload behind it must not remain publicly reachable on its own endpoint, or requests bypass inspection entirely.
- Only
*-caddy-alpine-*images work, and only a digest or a datecode is safe to pin. Moving tags such as-ltschange the image under a running deployment. See Choosing an Image Tag. - CRS rule updates require a deliberate
imagechange. That is the right default for a security control, but new CRS releases are not picked up on their own. - A request body too large to inspect inside
timeoutSecondsreturns 504 from the WAF, not from your application. Sizeresources.cpuandtimeoutSecondstogether — see Request Body Size and Timeout. - CRS blocks any request carrying an
Expect: 100-continueheader (rule920450), which some HTTP clients add for large uploads. Switch that one rule off in the custom rules secret if your clients send it. timeoutSecondsalso caps how long the protected workload may take to answer, not only how long inspection may take.- Custom rules layer on top of CRS, so a rule ID that collides with a CRS rule silently overrides it, and rule changes take effect only after the replicas restart.
- The WAF fails closed. If the startup hook cannot configure the reverse proxy, the container restarts rather than serving uninspected traffic. If the workload does not start, check
cpln logsfor a[FATAL]line naming the failed check; an incompatible non-Caddy image instead exits immediately withexitCode: 127after loggingLaunching caddy run …. - Do not override the container’s
commandorargs. They run the image’s own entrypoint behind atailthat forwards the startup hook’s output onto the container log; replacing them leaves a failed hook with no diagnosis.
External References
OWASP Coraza Documentation
Official Coraza WAF documentation and tutorials
OWASP CRS Documentation
Core Rule Set documentation and rule reference
seclang Directives
Directive reference for writing custom rules
Coraza CRS Docker
Image source, supported variants, and available tags
Caddy Admin API
The API the startup hook uses to configure the reverse proxy
Coraza Template
View the source files, default values, and chart definition