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:
64
scripts/backup.sh
Executable file
64
scripts/backup.sh
Executable file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
# ---------------------------------------------------------------------------
|
||||
# scripts/backup.sh — local SQLite + media snapshot
|
||||
#
|
||||
# Produces two artifacts under data/backups/:
|
||||
# app-<ts>.db : SQLite .backup of the live database (safe while
|
||||
# the app is running; sqlite3 .backup coordinates
|
||||
# with WAL).
|
||||
# media-<ts>.tar.gz : tar archive of the media upload directory.
|
||||
#
|
||||
# Retention: keep the 14 newest of each artifact; prune the rest.
|
||||
#
|
||||
# The script is intended to be invoked by cron on the Debian VM. Example
|
||||
# crontab line (daily at 03:15 UTC):
|
||||
# 15 3 * * * cd /srv/chicken_babies_site && ./scripts/backup.sh \
|
||||
# >> /var/log/chicken_babies/backup.log 2>&1
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 on success. Any failing step aborts via `set -e` with a non-zero
|
||||
# status so cron emails / log scrapers can flag it.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# UTC timestamp: sortable, compact, unambiguous. Matches the ISO-8601
|
||||
# "basic" form with a literal Z suffix.
|
||||
TS="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
|
||||
BACKUP_DIR="data/backups"
|
||||
DB_SRC="data/app.db"
|
||||
MEDIA_SRC="data/media"
|
||||
|
||||
# Ensure the output directory exists. `mkdir -p` is idempotent and never
|
||||
# fails if the directory already exists, which is exactly what we want.
|
||||
mkdir -p "${BACKUP_DIR}"
|
||||
|
||||
# --- SQLite snapshot ------------------------------------------------------
|
||||
# Use sqlite3's .backup command rather than `cp` so we get a consistent
|
||||
# copy even if the database is actively being written to. sqlite3 serialises
|
||||
# with the WAL and produces a file that's safe to open elsewhere.
|
||||
DB_DEST="${BACKUP_DIR}/app-${TS}.db"
|
||||
sqlite3 "${DB_SRC}" ".backup ${DB_DEST}"
|
||||
|
||||
# --- Media archive --------------------------------------------------------
|
||||
# tar the whole media tree. gzip compression keeps the archive small for
|
||||
# typical image payloads; restore is just `tar -xzf`.
|
||||
MEDIA_DEST="${BACKUP_DIR}/media-${TS}.tar.gz"
|
||||
tar -czf "${MEDIA_DEST}" "${MEDIA_SRC}"
|
||||
|
||||
# --- Retention prune -------------------------------------------------------
|
||||
# Keep the 14 newest of each type; delete the rest. ls -t sorts newest-
|
||||
# first; tail -n +15 drops the first 14 lines (the survivors). We guard
|
||||
# each rm with `|| true` so a cold start with fewer than 14 artifacts
|
||||
# doesn't cause the script to exit non-zero via `set -e`.
|
||||
keep_newest_14() {
|
||||
local pattern="$1"
|
||||
# shellcheck disable=SC2012 # `ls -t` is the simplest way to sort by mtime
|
||||
ls -1t ${pattern} 2>/dev/null | tail -n +15 | xargs -r rm -f
|
||||
}
|
||||
|
||||
keep_newest_14 "${BACKUP_DIR}/app-*.db"
|
||||
keep_newest_14 "${BACKUP_DIR}/media-*.tar.gz"
|
||||
|
||||
echo "backup complete: ${DB_DEST}, ${MEDIA_DEST}"
|
||||
Reference in New Issue
Block a user