> ## Documentation Index
> Fetch the complete documentation index at: https://controlplanecorporation-majid-docs-content-expansion.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Coraza WAF

> Deploy Coraza WAF with the OWASP Core Rule Set on Control Plane using the Template Catalog. A reverse proxy that inspects and filters traffic before forwarding it to a workload you already run.

## Overview

Coraza is an open-source web application firewall that ships with the OWASP Core Rule Set (CRS). This template deploys the [coraza-crs](https://github.com/coreruleset/coraza-crs-docker) 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](#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](https://coraza.io/docs/seclang/directives/) 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.

<Note>
  This template does not create a GVC. You must deploy it into an existing GVC alongside the workload you want to protect.
</Note>

## 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:

<CardGroup cols={2}>
  <Card title="UI" href="/template-catalog/install-manage/ui" icon="laptop">
    Browse, install, and manage templates visually
  </Card>

  <Card title="CLI" href="/template-catalog/install-manage/cli" icon="terminal">
    Manage templates from your terminal
  </Card>

  <Card title="Terraform" href="/template-catalog/install-manage/terraform" icon={<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><g fill-rule="evenodd"><path d="M77.941 44.5v36.836L46.324 62.918V26.082zm0 0" fill="#5c4ee5"/><path d="M81.41 81.336l31.633-18.418V26.082L81.41 44.5zm0 0" fill="#4040b2"/><path d="M11.242 42.36L42.86 60.776V23.941L11.242 5.523zm0 0M77.941 85.375L46.324 66.957v36.82l31.617 18.418zm0 0" fill="#5c4ee5"/></g></svg>}>
    Declare templates in your Terraform configurations
  </Card>

  <Card
    title="Pulumi"
    href="/template-catalog/install-manage/pulumi"
    icon={<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" id="Pulumi-Icon--Streamline-Svg-Logos" height="24" width="24">
    <desc>
        Pulumi Icon Streamline Icon: https://streamlinehq.com
    </desc>
    <path fill="#f26e7e" d="M4.683025 13.3318c0.869125 -0.5018 0.870575 -2.1264 0.003225 -3.62865s-2.27504 -2.313275 -3.1441725 -1.811475C0.672945 8.3935 0.6715 10.0181 1.53885 11.52035c0.86735 1.502275 2.27505 2.313275 3.144175 1.81145Zm0.0052 3.2167c0.86735 1.502275 0.865925 3.126875 -0.003225 3.628675 -0.86915 0.5018 -2.2768275 -0.309225 -3.144175 -1.81145 -0.8673525 -1.50225 -0.8659075 -3.126875 0.003225 -3.628675 0.8691325 -0.5018 2.276825 0.309225 3.144175 1.81145Zm5.922875 3.4243c0.86735 1.50225 0.8659 3.126775 -0.003225 3.62875 -0.869125 0.501775 -2.27685 -0.309325 -3.1442 -1.81155 -0.867325 -1.50225 -0.865875 -3.12685 0.00325 -3.628675 0.869125 -0.5018 2.276825 0.309225 3.144175 1.811475Zm-0.001925 -6.845275c0.86735 1.50225 0.8659 3.12685 -0.003225 3.628675 -0.869125 0.5018 -2.276825 -0.309225 -3.144175 -1.811475 -0.86735 -1.50225 -0.8659 -3.12685 0.003225 -3.62865 0.869125 -0.501825 2.276825 0.3092 3.144175 1.81145Z" stroke-width="0.25"></path>
    <path fill="#8a3391" d="M22.45775 11.524125c0.86725 -1.502225 0.865925 -3.12685 -0.003225 -3.62865 -0.869125 -0.501825 -2.276825 0.3092 -3.144175 1.811475 -0.86735 1.50225 -0.8659 3.126825 0.003225 3.62865 0.869125 0.501825 2.276825 -0.3092 3.144175 -1.811475Zm0.000175 3.2151c0.869075 0.5018 0.870625 2.1264 0.003225 3.62865 -0.86735 1.50225 -2.27505 2.313275 -3.144175 1.81145 -0.869125 -0.5018 -0.870575 -2.126425 -0.003225 -3.62865 0.86735 -1.50225 2.27505 -2.313275 3.144175 -1.81145ZM16.536225 18.157875c0.86915 0.501825 0.8706 2.126425 0.00325 3.628675 -0.86735 1.502125 -2.275075 2.313225 -3.1442 1.81145 -0.869125 -0.50175 -0.870575 -2.126425 -0.003225 -3.62865 0.867375 -1.502275 2.27505 -2.3133 3.144175 -1.811475Zm-0.003325 -6.843775c0.869125 0.5018 0.870575 2.126425 0.003225 3.628675s-2.27505 2.313275 -3.1442 1.811475c-0.869125 -0.501825 -0.870575 -2.126425 -0.003225 -3.628675 0.86735 -1.502275 2.27505 -2.313275 3.1442 -1.811475Z" stroke-width="0.25"></path>
    <path fill="#f7bf2a" d="M15.138225 2.06721c0 1.003615 -1.40625 1.817215 -3.14095 1.817215 -1.7347 0 -3.14095 -0.8136 -3.14095 -1.817215C8.856325 1.06359 10.262575 0.25 11.997275 0.25c1.7347 0 3.14095 0.81359 3.14095 1.81721ZM9.2166 5.482375c0 1.003625 -1.40625 1.8172 -3.14095 1.8172 -1.7347 0 -3.14095 -0.813575 -3.14095 -1.8172s1.40625 -1.817225 3.14095 -1.817225c1.7347 0 3.14095 0.8136 3.14095 1.817225Zm8.71005 1.8172c1.7347 0 3.14095 -0.813575 3.14095 -1.8172s-1.40625 -1.817225 -3.14095 -1.817225c-1.7347 0 -3.14095 0.8136 -3.14095 1.817225s1.40625 1.8172 3.14095 1.8172Zm-2.788425 1.605625c0 1.003625 -1.40625 1.8172 -3.14095 1.8172 -1.7347 0 -3.14095 -0.813575 -3.14095 -1.8172 0 -1.0036 1.40625 -1.8172 3.14095 -1.8172 1.7347 0 3.14095 0.8136 3.14095 1.8172Z" stroke-width="0.25"></path>
    </svg>}
  >
    Declare templates in your Pulumi programs
  </Card>
</CardGroup>

## Configuration

The default `values.yaml` for this template:

```yaml theme={null}
# Pinned by digest = tag 4.28-caddy-alpine-202608260808 (OWASP CRS 4.28.0, Caddy v2.11.3).
# Only the *-caddy-alpine-* variants work with this template, and only a datecode or a
# digest is safe to pin — moving tags such as -lts get repointed upstream.
image: ghcr.io/coreruleset/coraza-crs@sha256:ed1e4a656e83f1ba17d629f30de0910d491b1cc13e5e8e7e9b9f2ac9a35bac1b

# MUST BE CHANGED
targetWorkload: my-workload.my-gvc.cpln.local # Workload internal name of the workload to proxy traffic to

targetPort: 8080 # Port of the workload to proxy traffic to

WAFPort: 80 # Port on the WAF workload to expose to the internet

# Coraza inspects request bodies on the CPU, so these size the largest body the WAF
# can inspect inside timeoutSeconds. See "Request size, CPU and timeout" in the README
# before lowering them — 50m/128Mi, the pre-1.2.0 defaults, 504 on any body over ~30 KB.
resources:
  cpu: 500m
  memory: 512Mi

# Request timeout, in seconds, for everything passing through the WAF. It caps both the
# time Coraza spends inspecting a request body and the time the upstream has to answer;
# anything slower gets a 504 from the WAF, not from your application.
timeoutSeconds: 30

multiZone: false
```

### 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:

```text theme={null}
largest inspected body ≈ 120 KB × cpu-cores × timeoutSeconds
```

At the shipped defaults (`cpu: 500m`, `timeoutSeconds: 30`) that is about **1.8 MB**, measured against the public endpoint:

| POST body | Result            |
| --------- | ----------------- |
| 1 KB      | 200 (0.05 s)      |
| 50 KB     | 200 (0.88 s)      |
| 1500 KB   | 200 (26.19 s)     |
| 1700 KB   | 200 (29.77 s)     |
| 1900 KB   | **504** (30.04 s) |

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.

<Note>
  `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.
</Note>

### 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](#request-body-size-and-timeout).
* `multiZone` — When `true`, deploys replicas across multiple zones for higher availability.

<Note>
  Not all locations support multi-zone deployments. Confirm that your target location supports multi-zone before enabling this option.
</Note>

### 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`:

```text theme={null}
SecRule REQUEST_URI "@rx attack" "id:1001,phase:1,deny,msg:'Blocked attack attempt'"
```

Rules use [seclang directives](https://coraza.io/docs/seclang/directives/). The secret is loaded **after** the Core Rule Set, so it can also switch a CRS rule off:

```text theme={null}
SecRuleRemoveById 920450
```

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.

<Note>
  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.
</Note>

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:

```bash theme={null}
cpln logs '{gvc="GVC_NAME", workload="RELEASE_NAME-coraza-waf"}' --limit 100 --since 30m
```

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

| What                | Value                                                                                       |
| ------------------- | ------------------------------------------------------------------------------------------- |
| Public              | The WAF workload's canonical endpoint on `WAFPort` — this is the address clients should use |
| Internal (same GVC) | `RELEASE_NAME-coraza-waf.GVC_NAME.cpln.local:WAFPort`                                       |
| Upstream            | Whatever you set as `targetWorkload` and `targetPort`                                       |

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](#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](#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

<CardGroup cols={2}>
  <Card title="OWASP Coraza Documentation" icon="book" href="https://coraza.io/docs/tutorials/introduction/">
    Official Coraza WAF documentation and tutorials
  </Card>

  <Card title="OWASP CRS Documentation" icon="shield" href="https://coreruleset.org/docs/">
    Core Rule Set documentation and rule reference
  </Card>

  <Card title="seclang Directives" icon="list-check" href="https://coraza.io/docs/seclang/directives/">
    Directive reference for writing custom rules
  </Card>

  <Card title="Coraza CRS Docker" icon="docker" href="https://github.com/coreruleset/coraza-crs-docker">
    Image source, supported variants, and available tags
  </Card>

  <Card title="Caddy Admin API" icon="gear" href="https://caddyserver.com/docs/api">
    The API the startup hook uses to configure the reverse proxy
  </Card>

  <Card title="Coraza Template" icon="github" href="https://github.com/controlplane-com/templates/tree/main/coraza">
    View the source files, default values, and chart definition
  </Card>
</CardGroup>
