Skip to content

fix(waf): detect stale seed config and report version skew (#312, #316) - #385

Merged
bihius merged 5 commits into
mainfrom
fix/312-316-config-version-checks
Oct 22, 2026
Merged

bihius merged 5 commits into
mainfrom
fix/312-316-config-version-checks

Conversation

@bihius

@bihius bihius commented Oct 21, 2026

Copy link
Copy Markdown
Owner

Closes #312
Closes #316

Stacked on fix/321-hardening. This branch already contains the #321 merge commit, so the diff below is only #312/#316. Retarget the base to main once #321 merges; no rebase should be needed.

What changed

#312 — a tag-only upgrade is now loud instead of silently wrong

The release kit bind-mounts haproxy/coraza.cfg into HAProxy and the backend. Bumping only GUARD_PROXY_IMAGE_TAG therefore runs a new image against a pre-beta.5 file that sends app=str(default): every request is inspected by the default Coraza application instead of its vhost's policy_<id>, with no error anywhere (verified in the issue with real stacks).

  • All four shipped configs (configs/haproxy/, release/haproxy/, both files) carry # guard-proxy-config-version: 1. The generated haproxy.cfg template emits the same stamp from EXPECTED_CONFIG_VERSION, so the seed file still equals the rendered reference modulo whitespace.
  • check_seed_config() reads the mounted coraza.cfg at settings.seed_coraza_config_path (default /usr/local/etc/haproxy/coraza.cfg) and compares the stamp. A missing file is skipped, so local runs and pytest are unaffected; a stale one is a failure.
  • Check surfaces:
    • startup — one ERROR log naming the file;
    • GET /health — 200 with status: degraded and the stale path in detail;
    • GET /ready — the seed_config check fails, so the release kit's backend healthcheck fails; the body stays generic per Lower-severity hardening: write-time validation, log isolation, CORS, /ready detail #321's rule and the path goes to the server log;
    • POST /config/apply — 409 naming the file, before anything is written or reloaded;
    • GET /runtime/status — active_config.degraded plus warning naming the file (rendered by the existing admin-UI alert).

#316 — the CRS version comes from one place

  • app/constants/versions.py holds CRS_VERSION; crs-setup.conf.j2 takes ver:'OWASP_CRS/{{ crs_version }}' and the derived tx.crs_setup_version instead of hardcoded 4.25.0 / 4250. A test asserts the constant matches the configs/coraza/crs submodule's crs-setup.conf.example header, so bumping the submodule alone now fails a test. configs/coraza/crs-setup.conf stays the checked reference — the existing baseline test keeps it in sync.
  • GET /runtime/status gains version_agreement: app_version, configured_image_tag (from GUARD_PROXY_IMAGE_TAG, mapped v0.1.0-beta.10 → 0.1.0b10), crs_version, and a warning when the tag and the backend's own version disagree.
  • APP_VERSION moved to app/version.py (the status service needs it; importing it from app.main would be circular).

Fail-closed consequence

A stale stamp makes /ready fail, so frontend, haproxy, coraza and log-shipper (all depends_on: backend: service_healthy) do not start until the haproxy/ files from the same release are in place. That is the intended "refuses to report healthy" behaviour from #312; the backend log names the file, and the admin UI surfaces it on an already-running stack. Documented in release/README.md ("Upgrading", "A tag-only upgrade is refused") and docs/architecture.md.

Tests

cd src/backend && uv run ruff check app/ tests/ && uv run mypy app/ && uv run pytest -q

  • ruff: pass
  • mypy: pass (77 files)
  • pytest: 933 passed, 6 skipped, 4 deselected

New coverage: stale/older/newer/missing-stamp detection and all four shipped configs passing; /health degraded vs healthy; /ready 503 with a generic body, no path in the body, path in caplog; /config/apply 409 and 200; /runtime/status version_agreement and stale-seed warning; tag→PEP 440 mapping and skew warning; rendered CRS metadata using CRS_VERSION and matching the submodule.

Skipped

A tag-only upgrade of the release kit runs new images against the old
bind-mounted haproxy/ files. The old coraza.cfg sends app=str(default), so
every request is inspected by the default Coraza application instead of its
vhost's policy_<id>: traffic is still filtered, but with the wrong policy and
no error anywhere.

Stamp haproxy.cfg and coraza.cfg with `# guard-proxy-config-version: N` and
bake the expected version into the backend. The seed coraza.cfg is checked at
startup, in GET /health (degraded), in GET /ready (fails, so the release kit
backend healthcheck fails) and in POST /config/apply (409), naming the stale
file. Absent outside a release kit, so local runs are unaffected.

Also stop hardcoding the CRS version: the template now takes it from one
constant that a test pins to the configs/coraza/crs submodule, and
GET /runtime/status reports app_version, the configured image tag and the CRS
version, warning when the tag and the backend version disagree.

Closes #312
Closes #316
Reject CORS_ORIGINS='*' at startup since credentialed requests are always enabled. Return a generic /ready error detail and log the runtime config path server-side. Document global /logs access and the X-Forwarded-For trust dependency on HAProxy.
Copilot AI balanced review requested due to automatic review settings October 21, 2026 20:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@bihius
bihius added this pull request to stack #386 October 22, 2026 07:49
Base automatically changed from fix/321-hardening to main October 22, 2026 07:49
main added the seed-config readiness check in main.py while #384 made the
runtime-config failure detail generic and logged the path instead. Both
changes are kept: the git conflict was purely textual in the
`runtime_config` error branch.
@bihius
bihius merged commit e9716b7 into main Oct 22, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants