Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions dockerfiles/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,22 @@ fly -t <target> set-pipeline \
-c pipelines/infrastructure/ol_python_base_docker.yaml
```

## apisix-waf

[`apisix-waf/`](apisix-waf/) — Spike image layering the [Coraza](https://www.coraza.io/)
proxy-wasm WAF filter (OWASP Core Rule Set embedded at compile time by the
upstream `coraza-proxy-wasm` project) onto the stock `apache/apisix` image.
`wasm-nginx-module`/wasmtime are already compiled into every apisix-base
build, so this image only needs to copy the plugin binary in.

Published as `mitodl/apisix-waf` (ECR only, not Docker Hub) via the Concourse
pipeline at
[`pipelines/container_images/apisix_waf.py`](../src/ol_concourse/pipelines/container_images/apisix_waf.py).
Deliberately not chained to a Pulumi deploy -- see
`apisix_custom_image_repository`/`apisix_custom_image_tag` in
[`apisix_official.py`](../src/ol_infrastructure/infrastructure/aws/eks/apisix_official.py)
for the opt-in per-cluster config that actually references this image.

## edX / Open edX

The edX Dockerfiles that previously lived here (`openedx-edxapp`,
Expand Down
17 changes: 17 additions & 0 deletions dockerfiles/apisix-waf/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Spike: layers the Coraza WAF proxy-wasm filter (OWASP Core Rule Set embedded
# at compile time by the upstream project -- no separate ruleset files needed)
# onto the stock APISIX image. wasm-nginx-module/wasmtime are already compiled
# into every apisix-base build (confirmed against api7/apisix-build-tools'
# build-apisix-base.sh), so this needs nothing beyond copying the plugin binary
# in and registering it via Helm values (see apisix_official.py).
#
# Base tag must track APISIX_CHART_VERSION's default appVersion
# (src/bridge/lib/versions.py) -- chart 2.16.0 => APISIX 3.17.0-ubuntu.
ARG APISIX_IMAGE_TAG="3.17.0-ubuntu"
ARG CORAZA_PROXY_WASM_VERSION="0.6.0"

FROM ghcr.io/corazawaf/coraza-proxy-wasm:${CORAZA_PROXY_WASM_VERSION} AS coraza

FROM apache/apisix:${APISIX_IMAGE_TAG}

COPY --from=coraza /plugin.wasm /usr/local/apisix/coraza-filter.wasm
73 changes: 73 additions & 0 deletions src/ol_concourse/pipelines/container_images/apisix_waf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""Build and publish the apisix-waf spike image (APISIX + Coraza proxy-wasm).

Deliberately NOT chained into a pulumi_jobs_chain -- this is an opt-in spike
image. `apisix_official.py` only references it when a per-cluster Pulumi
stack config explicitly sets `apisix_custom_image_repository`, so image
builds here never trigger a deploy on their own.
"""

import sys

from ol_concourse.lib.containers import container_build_task, ensure_ecr_task
from ol_concourse.lib.models.pipeline import (
GetStep,
Identifier,
Input,
Job,
Pipeline,
PutStep,
)
from ol_concourse.lib.resources import git_repo, registry_image

from ol_concourse.pipelines.constants import ECR_REGION

ol_infrastructure_repo = git_repo(
name=Identifier("ol-infrastructure"),
uri="https://github.com/mitodl/ol-infrastructure",
branch="main",
paths=["dockerfiles/apisix-waf/Dockerfile"],
)

ecr_image_resource = registry_image(
name=Identifier("apisix-waf-image"),
image_repository="mitodl/apisix-waf",
ecr_region=ECR_REGION,
)


def build_job() -> Job:
context = f"{ol_infrastructure_repo.name}/dockerfiles/apisix-waf"
return Job(
name=Identifier("build-and-publish"),
public=True,
plan=[
GetStep(get=ol_infrastructure_repo.name, trigger=True),
container_build_task(
inputs=[Input(name=ol_infrastructure_repo.name)],
build_parameters={"CONTEXT": context},
),
ensure_ecr_task("mitodl/apisix-waf"),
PutStep(
put=ecr_image_resource.name,
inputs="detect",
params={
"image": "image/image.tar",
"additional_tags": f"{ol_infrastructure_repo.name}/.git/short_ref",
},
),
],
)


apisix_waf_pipeline = Pipeline(
resources=[ol_infrastructure_repo, ecr_image_resource],
jobs=[build_job()],
)

if __name__ == "__main__":
with open("definition.json", "w") as definition: # noqa: PTH123
definition.write(apisix_waf_pipeline.model_dump_json(indent=2))
sys.stdout.write(apisix_waf_pipeline.model_dump_json(indent=2))
sys.stdout.write(
"\nfly -t pr-inf set-pipeline -p apisix-waf-docker -c definition.json\n"
)
4 changes: 4 additions & 0 deletions src/ol_concourse/pipelines/container_images/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@
"ocw-course-publisher-image",
"src/ol_concourse/pipelines/container_images/ocw_course_publisher.py",
),
(
"apisix-waf-docker",
"src/ol_concourse/pipelines/container_images/apisix_waf.py",
),
]


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ config:
- tika-ci.ol.mit.edu
- video-ci.odl.mit.edu
- xpro-ci.odl.mit.edu
# Coraza WAF spike (ol-infrastructure#5101) -- pre-production only,
# low-risk long-horizon soak test before considering it for production.
# wasm.enabled loads the coraza-filter plugin globally but nothing
# routes through it yet, so this has no effect on live traffic.
eks:apisix_custom_image_repository: mitodl/apisix-waf
Comment thread
blarghmatey marked this conversation as resolved.
eks:apisix_custom_image_tag: latest
eks:apisix_wasm_plugins:
- name: coraza-filter
priority: 7999
file: /usr/local/apisix/coraza-filter.wasm
eks:developer_role_policy_name: AmazonEKSClusterAdminPolicy
eks:developer_role_kubernetes_groups:
- admin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ config:
- tika-qa.ol.mit.edu
- video-rc.odl.mit.edu
- xpro-rc.odl.mit.edu
# Coraza WAF spike (ol-infrastructure#5101) -- pre-production only,
# low-risk long-horizon soak test before considering it for production.
# wasm.enabled loads the coraza-filter plugin globally but nothing
# routes through it yet, so this has no effect on live traffic.
eks:apisix_custom_image_repository: mitodl/apisix-waf
Comment thread
blarghmatey marked this conversation as resolved.
eks:apisix_custom_image_tag: latest
eks:apisix_wasm_plugins:
- name: coraza-filter
priority: 7999
file: /usr/local/apisix/coraza-filter.wasm
eks:developer_role_policy_name: AmazonEKSClusterAdminPolicy
eks:developer_role_kubernetes_groups:
- admin
Expand Down
62 changes: 60 additions & 2 deletions src/ol_infrastructure/infrastructure/aws/eks/apisix_official.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from bridge.lib.magic_numbers import AWS_LOAD_BALANCER_NAME_MAX_LENGTH
from ol_infrastructure.lib.aws.eks_helper import (
cached_image_uri,
ecr_image_uri,
)
from ol_infrastructure.lib.ol_types import AWSBase
from ol_infrastructure.lib.pulumi_helper import StackInfo
Expand Down Expand Up @@ -211,6 +212,41 @@ def setup_apisix(
"""
apisix_domains = eks_config.get_object("apisix_domains") or []

# Opt-in, per-cluster: point at a custom-built APISIX image (e.g. the
# apisix-waf spike image at dockerfiles/apisix-waf/, published by
# src/ol_concourse/pipelines/container_images/apisix_waf.py) instead of
# the stock apache/apisix image, and/or register wasm plugins against it.
# Defaults to today's behavior (stock image, no wasm) on every cluster
# unless a stack explicitly sets these.
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
blarghmatey marked this conversation as resolved.
if apisix_custom_image_repository and not apisix_custom_image_tag:
msg = (
"apisix_custom_image_tag must be set when "
"apisix_custom_image_repository is set -- the custom image build "
"only publishes 'latest' and git-short-ref tags, not the stock "
"chart's default appVersion tag, so an untagged pull would fail."
)
raise ValueError(msg)
if apisix_custom_image_tag and not apisix_custom_image_repository:
msg = (
"apisix_custom_image_repository must be set when "
"apisix_custom_image_tag is set -- otherwise the tag is silently "
"applied to the stock apache/apisix image instead of the intended "
"custom image."
)
raise ValueError(msg)
if apisix_wasm_plugins and not apisix_custom_image_repository:
msg = (
"apisix_wasm_plugins requires apisix_custom_image_repository -- "
"the stock apache/apisix image does not contain any wasm plugin "
"binaries, so APISIX would fail to load the configured plugin file."
)
raise ValueError(msg)

session_cookie_name = f"{stack_info.env_suffix}_gateway_session".removeprefix(
"production"
).strip("_")
Expand Down Expand Up @@ -309,9 +345,31 @@ def setup_apisix(
values={
# --- Global/Image Configuration ---
"image": {
"repository": cached_image_uri("apache/apisix"),
"pullPolicy": "IfNotPresent",
"repository": (
ecr_image_uri(apisix_custom_image_repository)
if apisix_custom_image_repository
else cached_image_uri("apache/apisix")
),
**(
{"tag": apisix_custom_image_tag}
if apisix_custom_image_tag
else {}
Comment thread
blarghmatey marked this conversation as resolved.
),
# A custom image is expected to track a moving tag (e.g.
# "latest"), so always re-check the registry. The stock
# image path is unchanged from prior behavior -- it isn't
# digest-pinned either, cached_image_uri only rewrites the
# repository to our ECR pull-through cache, not a digest.
"pullPolicy": (
"Always" if apisix_custom_image_repository else "IfNotPresent"
),
},
# --- Wasm plugins (opt-in, see apisix_wasm_plugins above) ---
**(
{"wasm": {"enabled": True, "plugins": apisix_wasm_plugins}}
if apisix_wasm_plugins
else {}
),
# --- Autoscaling ---
"autoscaling": {
"enabled": True,
Expand Down