4 Commits

Author SHA1 Message Date
5aad7fd48f fix: trust X-Forwarded-* when Caddy is on another host
Site was loading under https but Starlette built logo/static URLs as
http, tripping CSP `img-src 'self'`. Root cause: Dockerfile hardcoded
`--forwarded-allow-ips 127.0.0.1`, but with Caddy on a separate host
(10.10.99.10) reverse-proxying to this VM, the container sees the
request's source IP as the Docker bridge gateway (~172.17.0.1), not
Caddy's real IP — uvicorn discarded X-Forwarded-Proto and fell back
to scheme=http.

Fixes both now and later:

- Dockerfile CMD switched to `sh -c exec uvicorn ... --forwarded-allow-ips
  "${FORWARDED_ALLOW_IPS:-127.0.0.1}"`. PID-1 / signal behaviour preserved
  via exec. Default stays 127.0.0.1 so host-uvicorn dev is unchanged;
  prod sets FORWARDED_ALLOW_IPS=* via .env or compose `command:`.
- docker-compose.prod.yml gets an explicit `command:` override so the
  currently-deployed image (with the hardcoded 127.0.0.1 flag) can be
  fixed without waiting for a rebuild. Also corrected the port binding
  comment + default: Caddy is off-host, so the port must be reachable
  over the LAN — restrict with host firewall / OPNsense instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 13:07:54 -05:00
62de2685d7 chore: move prod container port from 8000 to 8080
- Dockerfile: EXPOSE + HEALTHCHECK + uvicorn --port all shift to 8080.
- docker-compose.yml: host:container mapping shifts to 8080:8080.
- run_dev.sh: new host-dev launcher; default PORT=8080 to stay in
  sync with the prod shape.

Caddy upstream config on the VM needs to follow to 127.0.0.1:8080.
PUBLIC_BASE_URL in the production .env should reflect the public
hostname (unaffected by the internal port change).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 07:59:17 -05:00
f9f90d408e 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>
2026-04-22 07:38:23 -05:00
78dd1ac243 feat: phase 0 foundation — FastAPI scaffold + /healthz
Scaffold app/ package, pinned requirements.txt, multi-stage Dockerfile,
docker-compose.yml, and .env.example. Add typed pydantic-settings loader
with full env contract and a production validator that refuses the
dev-sentinel SECRET_KEY. Wire structlog with an APP_ENV-driven renderer
(console in dev, JSON in prod). Ship a minimal unauthenticated /healthz
returning {status, version, commit_sha} with commit SHA fed through a
GIT_COMMIT_SHA build arg.

Also mark Phase 0 complete in docs/ROADMAP.md.
2026-04-21 14:44:41 -05:00