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:
20
app/main.py
20
app/main.py
@@ -57,6 +57,7 @@ from app import __version__
|
||||
from app.config import get_settings
|
||||
from app.db import build_engine, run_migrations
|
||||
from app.logging_config import configure_logging
|
||||
from app.middleware import AccessLogMiddleware, SecurityHeadersMiddleware
|
||||
from app.models.seed import run_seed
|
||||
from app.routes.admin import router as admin_router
|
||||
from app.routes.admin_cms import router as admin_cms_router
|
||||
@@ -289,10 +290,29 @@ def create_app() -> FastAPI:
|
||||
application.state.admin_pages_service = admin_pages_service
|
||||
application.state.media_service = media_service
|
||||
|
||||
# --- Phase 6 middlewares -----------------------------------------------
|
||||
# Install order matters: FastAPI.add_middleware wraps each new entry
|
||||
# AROUND the existing stack, so the LAST one added runs first on the
|
||||
# request. We want:
|
||||
# - SecurityHeadersMiddleware innermost (stamps nonce on
|
||||
# request.state BEFORE the route runs, writes headers on the
|
||||
# response just before it exits the handler layer) -> added FIRST.
|
||||
# - AccessLogMiddleware outermost (times the entire stack,
|
||||
# including security-headers + CSRF cookie work) -> added LAST.
|
||||
application.add_middleware(
|
||||
SecurityHeadersMiddleware,
|
||||
production=(settings.app_env == "production"),
|
||||
)
|
||||
|
||||
# CSRF cookie middleware — narrow to admin GETs; everything else
|
||||
# passes through untouched so public routes are unaffected.
|
||||
application.add_middleware(CSRFCookieMiddleware, csrf_service=csrf_service)
|
||||
|
||||
# Access log — added last so it wraps (and thus times) every other
|
||||
# middleware. Skips /healthz to keep compose healthcheck noise out
|
||||
# of the log; redacts /admin/auth/consume/<token> paths.
|
||||
application.add_middleware(AccessLogMiddleware)
|
||||
|
||||
# SlowAPI limiter + exception handler. The limiter is a module-level
|
||||
# singleton in app.services.rate_limit (because @limiter.limit has
|
||||
# to be applied at endpoint-definition time, before include_router).
|
||||
|
||||
Reference in New Issue
Block a user