Docs

Integrate APISIX with Coraza WAF

Install a verified Coraza Proxy-WASM module in an isolated Apache APISIX quickstart, enforce a custom rule and OWASP CRS, validate blocked and allowed traffic, and restore the original configuration.

Applies to:Apache APISIX 3.18.0

Deployments:DockerRoles:Security engineerTime:25 minutesImpact:Changes live traffic

Goal

Load a checksum-verified Coraza Proxy-WASM module into an isolated APISIX quickstart. First enforce one explicit URI rule, then replace it with the bundled OWASP Core Rule Set (CRS), validate blocked and allowed requests against a controlled upstream, and restore the exact original configuration.

Coraza's maintainers state that a WAF requires workload-specific configuration and tuning; do not treat the bundled CRS as a production-ready policy. The 0.6.0 module also has unresolved upstream memory-usage reports. Load-test and tune it with representative traffic before production use.

Before you begin

  • Complete Get APISIX with the Docker quickstart.
  • Install Docker, cURL, jq, shasum, and unzip.
  • Use an isolated quickstart. This procedure temporarily changes the running APISIX configuration and traffic behavior.

Set the endpoints, release metadata, and Admin API key:

export APISIX_ADMIN_URL="http://127.0.0.1:9180"
export GATEWAY_URL="http://127.0.0.1:9080"
export CORAZA_VERSION="0.6.0"
export CORAZA_SHA256="cca4e3c75cf6b2e615907f936a1b6dcd0955250e0fb7d3b1c2ecef807d84603c"
read -rsp "Admin API key (press Enter for an unchanged quickstart): " APISIX_ADMIN_KEY && export APISIX_ADMIN_KEY && echo

Protect the fixed Route, container, APISIX module path, and local files:

test "$(curl -sS --connect-timeout 3 --max-time 10 -o /dev/null -w '%{http_code}' "${APISIX_ADMIN_URL}/apisix/admin/routes/coraza-guide-route" -H "X-API-KEY: ${APISIX_ADMIN_KEY}")" = 404 || { echo 'Stop: route coraza-guide-route already exists'; exit 1; }
test -z "$(docker ps -aq --filter name=^/coraza-guide-upstream$)" || { echo 'Stop: container coraza-guide-upstream already exists'; exit 1; }
docker exec apisix-quickstart test ! -e /usr/local/bin/coraza-proxy-wasm.wasm || { echo 'Stop: APISIX module path already exists'; exit 1; }
for file in coraza-proxy-wasm.zip coraza-proxy-wasm.wasm coraza-guide-config.yaml.backup coraza-guide-config.yaml.updated coraza-guide-body.txt; do
  test ! -e "${file}" || { echo "Stop: ${file} already exists"; exit 1; }
done

Steps

Step 1: Download the module safely

Download the pinned official release and require its known SHA-256 digest before extracting it:

curl --fail-with-body -L --connect-timeout 10 --max-time 120 \
  "https://github.com/corazawaf/coraza-proxy-wasm/releases/download/${CORAZA_VERSION}/coraza-proxy-wasm-${CORAZA_VERSION}.zip" \
  -o coraza-proxy-wasm.zip
printf '%s  %s\n' "${CORAZA_SHA256}" coraza-proxy-wasm.zip | shasum -a 256 -c -
unzip -q coraza-proxy-wasm.zip
test -s coraza-proxy-wasm.wasm

The digest above was calculated from the official 0.6.0 GitHub Release asset. Stop if verification fails; do not copy or execute the file.

What changed: a verified copy of the pinned WebAssembly module exists in the current directory.

Step 2: Preserve the complete APISIX configuration

Back up the exact configuration and stop if a top-level wasm block already exists, because that case requires a reviewed merge:

umask 077
docker cp apisix-quickstart:/usr/local/apisix/conf/config.yaml coraza-guide-config.yaml.backup
test "$(grep -Ec '^wasm:[[:space:]]*$' coraza-guide-config.yaml.backup)" = 0 || { echo 'Stop: merge with the existing top-level wasm configuration'; exit 1; }
cp coraza-guide-config.yaml.backup coraza-guide-config.yaml.updated
cat >> coraza-guide-config.yaml.updated <<'YAML'

wasm:
  plugins:
    - name: coraza-filter
      priority: 7999
      file: /usr/local/bin/coraza-proxy-wasm.wasm
YAML

Copy the module and updated configuration, validate them, and recover immediately if apisix test fails:

docker cp coraza-proxy-wasm.wasm apisix-quickstart:/usr/local/bin/coraza-proxy-wasm.wasm
docker cp coraza-guide-config.yaml.updated apisix-quickstart:/usr/local/apisix/conf/config.yaml
if ! docker exec apisix-quickstart apisix test; then
  docker cp coraza-guide-config.yaml.backup apisix-quickstart:/usr/local/apisix/conf/config.yaml
  docker exec apisix-quickstart rm -f /usr/local/bin/coraza-proxy-wasm.wasm
  docker exec apisix-quickstart apisix reload
  exit 1
fi
docker exec apisix-quickstart apisix reload

What changed: APISIX loads the verified module while retaining every pre-existing configuration field.

Step 3: Start a controlled upstream

Start a pinned local HTTP server and wait for its listener:

docker run -d --name coraza-guide-upstream \
  --network apisix-quickstart-net \
  hashicorp/http-echo:1.0.0 -listen=:5678 -text=coraza-guide-upstream
for attempt in {1..15}; do
  docker exec apisix-quickstart bash -c '</dev/tcp/coraza-guide-upstream/5678' >/dev/null 2>&1 && break
  sleep 1
done
docker exec apisix-quickstart bash -c '</dev/tcp/coraza-guide-upstream/5678'

What changed: allowed requests can be verified against a deterministic upstream without an external network dependency.

Step 4: Enforce one explicit rule

Create a fixed-ID Route. The rule lowercases the URI and denies paths that begin with /coraza-guide/archive:

curl -sS --connect-timeout 3 --max-time 15 "${APISIX_ADMIN_URL}/apisix/admin/routes/coraza-guide-route" -X PUT \
  -H "X-API-KEY: ${APISIX_ADMIN_KEY}" -H "Content-Type: application/json" \
  -d '{
    "uri": "/coraza-guide/*",
    "plugins": {
      "coraza-filter": {
        "conf": {
          "directives_map": {
            "default": [
              "SecDebugLogLevel 0",
              "SecRuleEngine On",
              "SecRule REQUEST_URI \"@beginsWith /coraza-guide/archive\" \"id:101,phase:1,t:lowercase,deny,status:403\""
            ]
          },
          "default_directives": "default"
        }
      }
    },
    "upstream": {
      "type": "roundrobin",
      "nodes": {"coraza-guide-upstream:5678": 1}
    }
  }' | jq -e '.value.id == "coraza-guide-route" and .value.plugins["coraza-filter"].conf.default_directives == "default"'

What changed: the custom Coraza policy protects only /coraza-guide/* and does not enable verbose debug logging.

Require the blocked path to return 403, then require an allowed path to reach the intended upstream:

test "$(curl -sS --connect-timeout 3 --max-time 15 -o /dev/null -w '%{http_code}' \
  "${GATEWAY_URL}/coraza-guide/archive/test")" = 403
test "$(curl --fail-with-body -sS --connect-timeout 3 --max-time 15 -o coraza-guide-body.txt -w '%{http_code}' \
  "${GATEWAY_URL}/coraza-guide/public")" = 200
test "$(cat coraza-guide-body.txt)" = "coraza-guide-upstream"

Step 5: Evaluate the bundled OWASP CRS

Replace the same guide-owned Route with the recommended base configuration and bundled CRS:

curl -sS --connect-timeout 3 --max-time 15 "${APISIX_ADMIN_URL}/apisix/admin/routes/coraza-guide-route" -X PUT \
  -H "X-API-KEY: ${APISIX_ADMIN_KEY}" -H "Content-Type: application/json" \
  -d '{
    "uri": "/coraza-guide/*",
    "plugins": {
      "coraza-filter": {
        "conf": {
          "directives_map": {
            "default": [
              "Include @recommended-conf",
              "Include @crs-setup-conf",
              "Include @owasp_crs/*.conf",
              "SecRuleEngine On"
            ]
          },
          "default_directives": "default"
        }
      }
    },
    "upstream": {
      "type": "roundrobin",
      "nodes": {"coraza-guide-upstream:5678": 1}
    }
  }' | jq -e '.value.id == "coraza-guide-route"'

What changed: the same bounded Route now evaluates the module's bundled CRS. This is an evaluation baseline, not a tuned production policy.

Validate

Require a representative XSS payload to be blocked, then confirm ordinary traffic remains allowed:

test "$(curl -sS --connect-timeout 3 --max-time 15 -o /dev/null -w '%{http_code}' \
  "${GATEWAY_URL}/coraza-guide/public" -H "Cookie: input=<body onload='alert(xss)'>")" = 403
test "$(curl --fail-with-body -sS --connect-timeout 3 --max-time 15 -o coraza-guide-body.txt -w '%{http_code}' \
  "${GATEWAY_URL}/coraza-guide/public")" = 200
test "$(cat coraza-guide-body.txt)" = "coraza-guide-upstream"

All assertions must pass. Review APISIX error logs for false positives and resource pressure during a broader, representative test before selecting production rules.

Roll back / Clean up

Delete the Route and upstream, restore the exact APISIX configuration, then remove the module and local files:

curl -sS --connect-timeout 3 --max-time 15 -X DELETE "${APISIX_ADMIN_URL}/apisix/admin/routes/coraza-guide-route" -H "X-API-KEY: ${APISIX_ADMIN_KEY}"
docker rm -f coraza-guide-upstream
docker cp coraza-guide-config.yaml.backup apisix-quickstart:/usr/local/apisix/conf/config.yaml
docker exec apisix-quickstart apisix test
docker exec apisix-quickstart apisix reload
docker exec apisix-quickstart rm -f /usr/local/bin/coraza-proxy-wasm.wasm
rm -f coraza-proxy-wasm.zip coraza-proxy-wasm.wasm coraza-guide-config.yaml.backup \
  coraza-guide-config.yaml.updated coraza-guide-body.txt
unset APISIX_ADMIN_URL APISIX_ADMIN_KEY GATEWAY_URL CORAZA_VERSION CORAZA_SHA256

Expected: the fixed Route, upstream, module, and local files are absent, and APISIX is running with the byte-for-byte pre-guide configuration.

Troubleshoot

SymptomCauseFix
SHA-256 verification fails.The download is incomplete or does not match the reviewed 0.6.0 asset.Delete the ZIP and stop. Re-establish trusted release provenance before updating the digest or version.
apisix test rejects the configuration.A top-level key conflicts or the module is incompatible with the running APISIX image.Use the automatic recovery result; do not reload the rejected configuration. Test compatibility in a separate environment.
The blocked request reaches the upstream.The module did not load, the Route does not contain the intended policy, or the request path does not match.Read the fixed Route and APISIX startup errors; do not weaken SecRuleEngine On.
Ordinary requests return 403.The CRS produces a false positive for the workload.Switch to DetectionOnly in an isolated tuning environment, inspect rule IDs, and create reviewed exclusions before enforcement.
Memory grows during load testing.The module or selected rules consume more memory than the deployment budget.Remove the Route and module using this rollback, capture a reproducible profile, and follow the linked upstream issue before production use.