Skip to main content

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 WAFPort and proxying to targetWorkload:targetPort. A standard workload that autoscales on CPU between one and three replicas.
  • Startup Hook — A postStart hook 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 on WAFPort, 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 reveal scoped 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 targetWorkload to that workload’s fully qualified internal address, WORKLOAD_NAME.GVC_NAME.cpln.local, and targetPort to the port it serves.
  • Set that workload’s internal firewall to same-gvc, same-org, or a workload-list that includes RELEASE_NAME-coraza-waf. The internal default is none, 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 default values.yaml for this template:

Proxy Target

  • targetWorkload — The internal DNS name of the workload to proxy traffic to, in the form WORKLOAD_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 (default 80).
Changing any of these re-runs the startup hook on the new replica, which re-resolves and re-patches the reverse proxy. Allow up to a few minutes after a 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:
At the shipped defaults (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 — When true, 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 named RELEASE_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:
Rules use seclang directives. The secret is loaded after the Core Rule Set, so it can also switch a CRS rule off:
CRS rule 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.
Custom rules layer on top of CRS, so a rule ID that collides with a CRS rule silently overrides it. Pick IDs outside the CRS ranges.

Logging

Coraza’s access, audit, and debug logs go to /dev/stdout and are readable through the built-in logging interface:
The startup hook’s own [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 -lts change the image under a running deployment. See Choosing an Image Tag.
  • CRS rule updates require a deliberate image change. 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 timeoutSeconds returns 504 from the WAF, not from your application. Size resources.cpu and timeoutSeconds together — see Request Body Size and Timeout.
  • CRS blocks any request carrying an Expect: 100-continue header (rule 920450), which some HTTP clients add for large uploads. Switch that one rule off in the custom rules secret if your clients send it.
  • timeoutSeconds also 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 logs for a [FATAL] line naming the failed check; an incompatible non-Caddy image instead exits immediately with exitCode: 127 after logging Launching caddy run ….
  • Do not override the container’s command or args. They run the image’s own entrypoint behind a tail that 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