chore: phase 6 hardening — CSP/HSTS, access log, docker, backup, CI

Ships the cross-cutting hardening set:

- SecurityHeadersMiddleware: per-request nonce-based CSP, HSTS
  (production only), Referrer-Policy, Permissions-Policy,
  X-Content-Type-Options, frame-ancestors 'none', form-action 'self'.
- AccessLogMiddleware: one http_request INFO event per request
  (method/path/status/duration_ms/ip/ua). Skips /healthz, redacts
  /admin/auth/consume/<token> paths, logs 500 + re-raises on
  downstream exceptions.
- Public base.html inline nav-toggle script gets a nonce so it
  passes strict CSP without relaxing to 'unsafe-inline'.
- Dockerfile: non-root app user (uid/gid 10001) + stdlib-only
  HEALTHCHECK against /healthz.
- scripts/backup.sh: sqlite3 .backup + tar data/media with
  14-entry retention; host-side cron install documented.
- .gitea/workflows/build-image.yml: on push to master /
  workflow_dispatch, builds and publishes
  git.sneakygeek.net/ptarrant/chicken_babies_site:latest +
  sha-<short>, with GIT_COMMIT_SHA threaded as a build-arg so
  /healthz keeps reporting the right commit in deployed images.
- 8 new tests (security headers + access log).

Pre-existing dev failures (logo asset rename + RESEND env
pollution) remain unchanged; verified not Phase 6 regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-22 07:38:23 -05:00
parent f4dc6c266d
commit f9f90d408e
12 changed files with 756 additions and 8 deletions

View File

@@ -0,0 +1,26 @@
"""Application-level Starlette middlewares.
Each middleware is a :class:`starlette.middleware.base.BaseHTTPMiddleware`
subclass wired up in :mod:`app.main`. They are kept in their own package
(rather than buried in ``app/main.py``) because Phase 6 introduced two
cross-cutting middlewares — security headers and access logging — that
benefit from isolated unit tests and clear homes for future additions
(rate-limit re-shaping, request-id propagation, etc.).
Public surface:
- :class:`SecurityHeadersMiddleware` — per-request CSP nonce + strict
response headers. See :mod:`app.middleware.security_headers`.
- :class:`AccessLogMiddleware` — structured ``http_request`` log line
after every response. See :mod:`app.middleware.access_log`.
"""
from __future__ import annotations
from app.middleware.access_log import AccessLogMiddleware
from app.middleware.security_headers import SecurityHeadersMiddleware
__all__ = [
"AccessLogMiddleware",
"SecurityHeadersMiddleware",
]