Adds (Dockerfile) and updates (coraza-spoa/Dockerfile) the OCI image.source label to point at github.com/shadowdao/haproxy-manager-base. ghcr.io auto-links a package to a GitHub repo when this label resolves to a github.com URL whose owner+name match the package's owner — that makes the published packages show up on the GitHub repo sidebar and inherit its collaborator settings. Gitea's registry ignores image.source, so changing the value away from the previous Gitea URL costs nothing on that side. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
coraza-spoa sidecar
A sidecar container that runs Coraza-SPOA as a WAF engine for haproxy-manager. HAProxy consults it per-request via the SPOE/SPOP protocol; Coraza evaluates the request against OWASP CRS rules and tells HAProxy whether to allow or block.
Design constraints
haproxy-managerdoes NOT depend on this sidecar. The base image works standalone (used in other projects and home networks) without WAF. SPOE config in the generatedhaproxy.cfgis opt-in via an env var onhaproxy-manager.- Fail-open when the sidecar is unhealthy.
option set-on-error continuein the HAProxy SPOE config means request flow continues uninspected if coraza-spoa is unreachable, rather than 503-ing customer traffic. - Detect-only globally; enforce explicitly. See
overrides.conffor the day-one enforce list. Most CRS rules log without blocking until we've tuned per-customer false positives.
Deployment shape
Two containers per host, both on the client-net docker network:
haproxy-manager (existing) — ports 80, 443, 8000
│ SPOE TCP/9000 → reach coraza-spoa by container DNS
▼
coraza-spoa (this image)
port 9000 (SPOE) — NOT exposed on host; internal network only
/var/log/coraza — bind-mounted to host for AI Monitor consumption
Typical docker run:
mkdir -p /var/log/coraza
chown 65532:65532 /var/log/coraza # distroless nonroot UID
docker run -d \
--name coraza-spoa \
--network client-net \
--restart unless-stopped \
-v /var/log/coraza:/var/log/coraza \
your-registry.example.com/cloud-hosting-platform/coraza-spoa:latest
Then on the haproxy-manager container, add the env var:
-e HAPROXY_CORAZA_SPOE_BACKEND=coraza-spoa:9000
The haproxy-manager template engine sees the env var and renders the SPOE config block pointing at this sidecar. Without the env var, no SPOE blocks render — the haproxy-manager image's behavior is unchanged.
Files
| File | Purpose |
|---|---|
Dockerfile |
Multi-stage build (golang:1.25 → distroless), pinned to upstream coraza-spoa tag |
config.yaml |
SPOA listener config + one named application haproxy |
overrides.conf |
Day-one enforce list (ctl:ruleEngine=On for high-confidence rule IDs) |
README.md |
This file |
Audit log
/var/log/coraza/audit.log — JSON, one event per line, RelevantOnly (only requests that triggered ≥1 rule are logged). AI Monitor should be configured to tail this on each host.
Entries include rule IDs, matched patterns, request metadata, and action taken (log for detect-only, deny for enforced). Use the JSON action field to filter blocked vs. observed.
Upgrading the pin
CRS rules are bundled into the coraza-spoa binary at build time, so the CRS version is whatever ships with the pinned coraza-spoa tag. To upgrade:
- Check upstream releases: https://github.com/corazawaf/coraza-spoa/releases
- Skim the CHANGELOG for new/changed rules in the
overrides.confID ranges. - Bump
ARG CORAZA_SPOA_VERSIONin the Dockerfile. - Push to
main— the Gitea workflow at.gitea/workflows/build-push-coraza.yamlrebuilds + pushes:latest. - On each host, run
container-manager.sh recreate coraza-spoato pull the new image.
Tuning false positives
When a legitimate request triggers a blocked rule, the audit log shows the rule ID. Two ways to silence it:
- Per-rule exception in
overrides.conf:SecRuleRemoveById <id>(full disable) orSecRuleRemoveTargetById <id> "<target>"(targeted exception). - Drop from the enforce list: remove the rule's ID range from the
ctl:ruleEngine=Onoverrides; it falls back to detect-only.
After tuning, push the change — CI rebuilds, then recreate coraza-spoa on each host to apply.