Skip to content

spike(apisix): opt-in Coraza WAF proxy-wasm image + Helm wiring - #5101

Open
blarghmatey wants to merge 5 commits into
mainfrom
spike/apisix-coraza-waf
Open

spike(apisix): opt-in Coraza WAF proxy-wasm image + Helm wiring#5101
blarghmatey wants to merge 5 commits into
mainfrom
spike/apisix-coraza-waf

Conversation

@blarghmatey

@blarghmatey blarghmatey commented Jul 23, 2026

Copy link
Copy Markdown
Member

What are the relevant tickets?

N/A. Follow-up spike from the investigation on #5096/#5099 (bot/scanner noise mitigation at the APISIX layer).

Description (What does it do?)

Spike, not a full rollout: builds a custom APISIX image bundling the Coraza WAF proxy-wasm filter, registers its build pipeline, and opts pre-production applications clusters into a low-risk long-horizon soak test before considering it for production.

Why Coraza over other options evaluated:

  • chaitin-waf is already in our loaded APISIX plugin list, but it isn't self-contained — it proxies every request to a separate self-hosted Chaitin SafeLine WAF detection-engine service (APISIX >=3.5.0, SafeLine >=5.6.0) that would need to be deployed and operated as new infrastructure.
  • Coraza runs in-process as a proxy-wasm filter — no separate service, no network hop per request.
  • Confirmed against the actual upstream build script (api7/apisix-build-tools's build-apisix-base.sh) that wasm-nginx-module is compiled into every standard apisix-base build unconditionally (--add-module=../wasm-nginx-module-..., not an opt-in variant) — so the wasm/proxy-wasm runtime our stock apache/apisix image already ships is sufficient; no custom OpenResty compile needed.
  • The only gap is getting the compiled coraza-proxy-wasm.wasm binary into the image. Confirmed via the coraza-proxy-wasm project's own README that OWASP Core Rule Set "comes embedded in the extension" at compile time by the upstream project — no separate ruleset files to deliver or mount.

Changes:

  • dockerfiles/apisix-waf/Dockerfile: two-stage build — COPY --from=ghcr.io/corazawaf/coraza-proxy-wasm:0.6.0 /plugin.wasm onto apache/apisix:3.17.0-ubuntu (tracks APISIX_CHART_VERSION's default appVersion).
  • src/ol_concourse/pipelines/container_images/apisix_waf.py: build-and-push-to-ECR pipeline, following the existing ol_python_base.py/kubewatch pattern (container_build_task + ensure_ecr_task + PutStep). Published as mitodl/apisix-waf:latest (+ git-short-ref secondary tag), ECR only (not Docker Hub — internal security tooling, not meant for public distribution). Deliberately not chained into a pulumi_jobs_chain — image builds here never trigger a deploy on their own.
  • src/ol_concourse/pipelines/container_images/meta.py: registers apisix-waf-docker in PIPELINE_CONFIGS so the meta-pipeline auto-creates/updates it on merge, same as every other image-build pipeline.
  • src/ol_infrastructure/infrastructure/aws/eks/apisix_official.py: new opt-in, per-cluster Pulumi config (apisix_custom_image_repository, apisix_custom_image_tag, apisix_wasm_plugins) that only diverges from today's behavior when a stack explicitly sets it, following the existing eks_config.get(...) or <default> pattern already used throughout this file for other per-cluster tunables (apisix_min_replicas, apisix_memory, etc.). pullPolicy switches to Always when a custom image is configured (since :latest needs re-checking on pod restart); the stock image path is unaffected.
  • Pulumi.applications.CI.yaml / Pulumi.applications.QA.yaml: opt in with the three new config keys. applications.Production is deliberately not touched.
  • dockerfiles/README.md: documents the new image per the directory's existing convention.

Why this is low-risk despite being "live" on two clusters: wasm.enabled: true loads the coraza-filter plugin globally, but no ApisixRoute or shared-plugin-config references it by name yet, so it's inert with respect to live traffic. What this soak-tests is narrower and specific: does APISIX start up and run stably over time on the custom image with the wasm runtime active. That's the operational confidence needed before attaching the plugin to any real route.

Screenshots (if appropriate):

N/A - infrastructure/backend only.

How can this be tested?

  • pulumi preview against applications.QA and applications.CI, both configured: identical isolated diff on each (image.repository/image.tag/image.pullPolicy + new wasm block), 1 resource updated, 167 unchanged.
  • pulumi preview against applications.Production: zero apisix-related diff (only the unrelated, already-known pending Traefik NLB fix from fix(eks): preserve real client IPs on the Traefik gateway NLB #5099 shows up).
  • Dockerfile passes the repo's pre-commit Dockerfile-linting hook; all changed Python/YAML files pass ruff, mypy, and yamllint.

Not done in this PR (deliberately, this is a spike, not a full rollout):

  • The Concourse pipeline is registered but hasn't been run yet on merge to actually confirm — first real build happens on merge via the meta-pipeline.
  • No route has coraza-filter attached — it isn't wired into the shared OLApisixSharedPluginsConfig any app depends on, and won't be until the soak test on CI/QA shows it's stable.
  • Production is not opted in.

Additional Context

Suggested next step after this merges and the soak test has run for a while with no stability issues: attach coraza-filter to a single low-traffic test route on one of the opted-in clusters (directives: Include @demo-conf, Include @crs-setup-conf, SecRuleEngine On, Include @owasp_crs/*.conf) to confirm it actually blocks known scanner traffic patterns (/wls-wsat/, /seeyon/htmlofficeservlet, etc.) before considering it for the shared plugin config any real app traffic depends on.

blarghmatey and others added 2 commits July 23, 2026 14:29
Spike for mitigating internet bot/vulnerability-scanner noise at the APISIX
gateway layer (see the QA rollout investigation for #5096/#5099). Evaluated
chaitin-waf (already loaded in our plugin list) and other self-hosted WAFs
first, but chaitin-waf requires a separate SafeLine detection-engine service
to operate. Coraza runs in-process as a proxy-wasm filter instead -- no
separate service, and the wasm runtime it needs is already compiled into
every standard apisix-base build (confirmed against api7/apisix-build-tools'
build-apisix-base.sh: wasm-nginx-module is an unconditional --add-module,
not an opt-in variant), so the only gap is getting the compiled
coraza-proxy-wasm binary into the image.

dockerfiles/apisix-waf/Dockerfile: two-stage build, copies the pre-built
coraza-proxy-wasm.wasm binary (OWASP Core Rule Set is embedded in it at
compile time by the upstream project -- confirmed via their README, no
separate ruleset files needed) from ghcr.io/corazawaf/coraza-proxy-wasm onto
the stock apache/apisix image. Published as mitodl/apisix-waf (ECR only) via
src/ol_concourse/pipelines/container_images/apisix_waf.py, following the
ol_python_base.py/kubewatch build-and-push pattern. Deliberately not chained
to a pulumi_jobs_chain -- image builds here never trigger a deploy on their
own.

apisix_official.py: new opt-in, per-cluster config
(apisix_custom_image_repository/apisix_custom_image_tag/apisix_wasm_plugins)
that only diverges from today's stock-image, wasm-disabled behavior when a
stack explicitly sets it -- verified zero-diff on applications.QA with
nothing set, and a clean isolated image/wasm diff when simulating the
opt-in values via CLI config overrides (not persisted to any stack file).

Next step to actually pilot this: set the three config keys on one QA
cluster and attach the coraza-filter plugin to a low-traffic test route to
validate it blocks known scanner patterns before considering it for any
shared plugin config.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…lications clusters

Registers apisix-waf-docker with the container-images-meta pipeline
(container_images/meta.py's PIPELINE_CONFIGS) so it gets auto-created on
merge, following every other entry's pattern.

Opts applications.CI and applications.QA into the Coraza image + wasm
plugin registration for a low-risk, long-horizon soak test:
apisix_custom_image_repository/apisix_custom_image_tag point APISIX at
mitodl/apisix-waf:latest, and apisix_wasm_plugins loads the coraza-filter
plugin. wasm.enabled loads the plugin globally, but nothing routes through
it yet -- no ApisixRoute or shared-plugin-config references coraza-filter
by name -- so this validates the image/wasm-runtime path stays stable over
time without touching live request handling.

Deliberately scoped to pre-production only. applications.Production
verified untouched by preview (its only pending diff is the unrelated,
already-known Traefik NLB fix from #5099).

apisix_official.py: pullPolicy switches to "Always" when a custom image is
configured, since :latest needs re-checking on each pod restart -- the
stock image path is unaffected (still IfNotPresent, already digest-cached
via cached_image_uri).

Verified via preview on all three stacks:
- applications.QA / applications.CI: identical isolated diff (image
  repository/tag/pullPolicy + new wasm block), 1 resource updated each.
- applications.Production: zero apisix-related diff.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@blarghmatey
blarghmatey marked this pull request as ready for review July 23, 2026 18:40
Copilot AI review requested due to automatic review settings July 23, 2026 18:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in path to run APISIX with the Coraza proxy-wasm WAF filter by (a) defining a custom image build pipeline and Dockerfile, and (b) wiring Helm values in the EKS APISIX deployment to optionally reference that image and register wasm plugins.

Changes:

  • Introduces a new apisix-waf Dockerfile and a Concourse pipeline to build/publish the image to ECR.
  • Adds per-stack Pulumi config knobs to optionally override the APISIX image repo/tag and enable wasm plugins.
  • Updates applications CI/QA stack configs to set those new knobs (currently contradicting the PR’s “no cluster opted-in” description).

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/ol_infrastructure/infrastructure/aws/eks/Pulumi.applications.QA.yaml Opts the QA stack into the custom APISIX WAF image + wasm plugin configuration.
src/ol_infrastructure/infrastructure/aws/eks/Pulumi.applications.CI.yaml Opts the CI stack into the custom APISIX WAF image + wasm plugin configuration.
src/ol_infrastructure/infrastructure/aws/eks/apisix_official.py Adds opt-in Helm wiring for custom image repo/tag and wasm plugins.
src/ol_concourse/pipelines/container_images/meta.py Registers the new apisix-waf container-image pipeline in the container-images meta pipeline list.
src/ol_concourse/pipelines/container_images/apisix_waf.py Defines the Concourse pipeline to build and push the apisix-waf image to ECR.
dockerfiles/README.md Documents the new apisix-waf image and how it’s published.
dockerfiles/apisix-waf/Dockerfile Adds the two-stage image that copies the Coraza .wasm artifact into an APISIX base image.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/ol_infrastructure/infrastructure/aws/eks/apisix_official.py
Addresses Copilot review feedback on #5101: repository set without a tag,
or wasm_plugins set without a custom image, would previously fail at
Helm-apply/pod-startup time (ImagePullBackOff pulling an untagged image
that doesn't exist under our tag scheme, or APISIX failing to find a wasm
plugin file the stock image never had) instead of at pulumi preview/up.

Verified both invalid combinations raise and both valid combinations
(unconfigured, and applications.QA/CI's actual config) pass -- re-previewed
applications.QA, same clean diff as before this change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

src/ol_infrastructure/infrastructure/aws/eks/apisix_official.py:225

  • apisix_custom_image_repository and apisix_custom_image_tag are treated as independent optional config values, but an incomplete configuration (only one set) will produce invalid Helm values (e.g., custom repo with chart default tag that likely doesn’t exist, or overriding the stock image tag unintentionally). Consider enforcing that these two settings must be provided together so misconfiguration fails fast during preview/up.
    apisix_custom_image_repository = eks_config.get("apisix_custom_image_repository")
    apisix_custom_image_tag = eks_config.get("apisix_custom_image_tag")
    # e.g. [{"name": "coraza-filter", "priority": 7999,
    #        "file": "/usr/local/apisix/coraza-filter.wasm"}]
    apisix_wasm_plugins = eks_config.get_object("apisix_wasm_plugins") or []

Comment thread src/ol_infrastructure/infrastructure/aws/eks/apisix_official.py Outdated
Addresses Copilot review feedback on #5101: cached_image_uri() only
rewrites the repository to our ECR pull-through cache -- it doesn't pin a
digest. The stock image path was never digest-pinned; wording corrected to
not imply otherwise.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread src/ol_infrastructure/infrastructure/aws/eks/apisix_official.py
blarghmatey added a commit that referenced this pull request Jul 23, 2026
Addresses Sentry AI review feedback on #5101 (bug prediction, low severity
but real): the existing validation only checked repository-without-tag,
missing the inverse -- apisix_custom_image_tag set without
apisix_custom_image_repository silently applies the tag to the stock
apache/apisix image instead of raising, since the two Helm values keys are
built from independent conditionals.

Verified all six config combinations (three invalid, three valid including
applications.QA/CI's actual config) behave correctly, and re-previewed
applications.QA -- diff unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Addresses Sentry AI review feedback on #5101 (bug prediction, low severity
but real): the existing validation only checked repository-without-tag,
missing the inverse -- apisix_custom_image_tag set without
apisix_custom_image_repository silently applies the tag to the stock
apache/apisix image instead of raising, since the two Helm values keys are
built from independent conditionals.

Verified all six config combinations (three invalid, three valid including
applications.QA/CI's actual config) behave correctly, and re-previewed
applications.QA -- diff unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@blarghmatey
blarghmatey force-pushed the spike/apisix-coraza-waf branch from c8a45ec to f80fae2 Compare July 23, 2026 19:49
@shaidar

shaidar commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

This should be turned into a project that includes some developers for a full blown assessment on how best to handle this. We already have existing tools/services in place that could be leveraged, however there's an additional cost associated with enabling those features. There are also some "free" tools such as the one proposed and others that could perform the job however it's added complexity and additional things to manage.
Turning this into an official project allows us to assess what the associated costs are, what are we willing to accept/live with, and make a decision on whether we spend the extra money or go down the path of tool assessment and deployment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants