← Back

Patch notes — image & runtime hardening (2026-06-19)

A hardening batch on top of the 2026-06-18 security fixes: the whole site now carries defensive HTTP headers, server-side probes are SSRF-guarded, and the container image was rebuilt onto a distroless base that ships 0 Critical/High CVEs and runs as a non-root user. All deployed to main. Docker smoke 157/0 throughout.

1 — Site-wide security headers ✅

X-Content-Type-Options: nosniff, X-Frame-Options: SAMEORIGIN and Referrer-Policy: strict-origin-when-cross-origin are now emitted on every response. They previously only covered the dynamic /admin + /api responses; the static Hugo marketing pages were served straight by the stdlib file handler and got none, leaving them clickjackable. Moved the headers into a single end_headers() override so static, dynamic, redirect and error responses are all covered uniformly. SAMEORIGIN (not DENY) so the admin’s own newsletter/content preview iframes keep working. Commit 2cf7fd4.

2 — SSRF guard on server-side probes ✅

Admin-supplied monitor / api-source / deployment URLs are fetched server-side by the in-process deploy checker and the collector. Added probe_url_allowed / probe_host_allowed: each target host is resolved and rejected if it lands on link-local (incl. the 169.254.169.254 cloud-metadata endpoint), loopback, unspecified, multicast or reserved ranges. Private LAN is intentionally allowed — probing internal hosts is what a monitoring tool does. Guards both the web checker (_http_check / _ssl_days_left) and the collector’s monitor + api-source fetches. Commit f3f8624.

3 — Distroless runtime image (0 Critical/High CVEs, non-root) ✅

Rebuilt the runtime from python:3.13-slim (Debian trixie) onto a 3-stage build ending on gcr.io/distroless/cc-debian12 with our own Python 3.13. Outcome, measured with Trivy:

Before (slim)After (distroless)
Tracked CVEs~56~14
Critical / High2 / 90 / 0
Image size368 MB69 MB
Runs asrootnon-root (uid 65532)
Shell / apt / perl / tarpresentremoved

Context: a CVE scan found all 56 base-image CVEs were inherited from Debian and none had an upstream fix available, so a base swap (not a rebuild) was the only lever. The two Criticals were perl-Archive-Tar (CVE-2026-42496/42497) — perl is gone entirely from the distroless image. ping-type monitors are unaffected (neither image ever shipped ping). Commit 1d7d3a1.

4 — Supporting fixes ✅

  • collector/fallback entrypoint — the distroless image sets an ENTRYPOINT (no shell), so the compose command: overrides would have silently run the web server in the collector and 404-fallback containers. Switched both to entrypoint:; verified the collector runs its loop and the fallback serves the branded 404. Commit 4aee470.
  • CI image tag — now YYYY.MM.DD.<run_number> (unique, sortable) so tag-based rollback actually works; the old logic always produced .1. Commit a26404d.
  • compose.dev.yml — runs Postgres now; it was stale SQLite-era and couldn’t boot the current code. Commit 10013f7.
  • Dropped vestigial ETHICA_DB_PATH — a SQLite path in a Postgres-only app, read nowhere. Commit 4d040a9.

Verification

Docker smoke 157/0 on every step; full py_compile clean; newsletter (7) and docs_reader (30) unit tests pass; the distroless image was booted end-to-end (web + collector + fallback) before deploy.