MosswartOverlord/scripts/backup-databases.sh
Erik a28b61511c security: enforce real plugin secret, fix proxy auth bypass, loopback DB ports, nightly backups
- SHARED_SECRET now read from env and fail-closed: unset/placeholder refuses
  ALL plugin connections (constant-time compare). The old hardcoded
  'your_shared_secret' in this public repo was no auth at all. Dockerfile
  default removed; generate_data.py reads the env var.
- SECRET_KEY fails closed at startup (main.py and agent/auth.py) instead of
  falling back to a publicly-known signing key; agent systemd unit now
  requires /etc/overlord/agent.env (no '-' prefix).
- AuthMiddleware + /ws/live: replace the 172.x source-IP trust (which every
  nginx-proxied internet request satisfied via docker-proxy — full session
  bypass and unauthenticated in-game command injection) with
  private-source AND no X-Forwarded-For, i.e. only genuinely internal
  callers (overlord-agent on the host, compose-network services). Invariant
  documented in nginx/overlord.conf: every tracker-bound location must set
  X-Forwarded-For.
- /character-stats/test endpoints gated behind admin (they upsert real rows).
- docker-compose: bind 5432/5433 to 127.0.0.1 (both DBs were internet-
  reachable; active brute-force observed in dereth-db logs).
- discord-rare-monitor: drop dead SHARED_SECRET constant.
- scripts/backup-databases.sh + docs/backups.md: nightly pg_dump of both DBs
  (telemetry/spawn hypertable data excluded), 10MB canary, umask 077,
  TimescaleDB restore procedure.
- Remove stray mangled-path css file from repo root.

Adversarially reviewed pre-deploy (3-lens workflow): ship verdict; deploy-
sequencing blockers addressed (secret staged before enforcement, exec bit
set, cron uses bash).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 17:02:47 +02:00

53 lines
2.6 KiB
Bash
Executable file

#!/usr/bin/env bash
# Nightly logical backups for both MosswartOverlord databases.
# Install as a cron job on the live host (see docs/backups.md). Note `bash`
# in the cron line (survives a lost executable bit) and that /home/erik/backups
# must exist BEFORE the first run (cron sets up the >> redirection before this
# script's mkdir runs):
# 15 3 * * * bash /home/erik/MosswartOverlord/scripts/backup-databases.sh >> /home/erik/backups/backup.log 2>&1
#
# What is backed up:
# - dereth (TimescaleDB): full schema + all data EXCEPT the raw
# telemetry_events/spawn_events hypertable chunks. Those tables hold
# ~12 GB of data that expires via retention policies in 7-30 days
# anyway; the irreplaceable rows (users, char_stats, rare_stats,
# rare_events, combat_stats*, portals, character_stats, server_status)
# are all included.
# - inventory_db (postgres): full dump (~1 GB raw, much smaller compressed).
#
# Restore procedure: docs/backups.md (TimescaleDB needs pre/post restore calls).
set -euo pipefail
# Dumps contain the users table (bcrypt hashes) — keep them owner-only.
umask 077
BACKUP_DIR="${BACKUP_DIR:-/home/erik/backups/postgres}"
KEEP_DAYS="${KEEP_DAYS:-7}"
STAMP="$(date -u +%Y%m%d-%H%M)"
mkdir -p "$BACKUP_DIR"
# dereth: -Fc is compressed; exclude hypertable chunk DATA (schema kept so a
# restore recreates the tables empty and retention/compression jobs reattach).
docker exec dereth-db pg_dump -U postgres -Fc \
--exclude-table-data='public.telemetry_events' \
--exclude-table-data='public.spawn_events' \
--exclude-table-data='_timescaledb_internal._hyper_*' \
dereth > "$BACKUP_DIR/dereth-$STAMP.dump.tmp"
# Canary: a healthy dereth dump is ~50 MB; a tiny one means pg_dump silently
# produced garbage (fail the run so the old dumps are kept and cron logs it).
if [ "$(stat -c%s "$BACKUP_DIR/dereth-$STAMP.dump.tmp")" -lt 10000000 ]; then
echo "$(date -u +%FT%TZ) FAIL dereth dump under 10MB — keeping old backups" >&2
exit 1
fi
mv "$BACKUP_DIR/dereth-$STAMP.dump.tmp" "$BACKUP_DIR/dereth-$STAMP.dump"
docker exec inventory-db pg_dump -U inventory_user -Fc inventory_db \
> "$BACKUP_DIR/inventory-$STAMP.dump.tmp"
mv "$BACKUP_DIR/inventory-$STAMP.dump.tmp" "$BACKUP_DIR/inventory-$STAMP.dump"
# Retention: keep KEEP_DAYS days of dailies.
find "$BACKUP_DIR" -name 'dereth-*.dump' -mtime +"$KEEP_DAYS" -delete
find "$BACKUP_DIR" -name 'inventory-*.dump' -mtime +"$KEEP_DAYS" -delete
# Clean up aborted runs older than a day.
find "$BACKUP_DIR" -name '*.dump.tmp' -mtime +1 -delete
echo "$(date -u +%FT%TZ) OK dereth=$(du -h "$BACKUP_DIR/dereth-$STAMP.dump" | cut -f1) inventory=$(du -h "$BACKUP_DIR/inventory-$STAMP.dump" | cut -f1)"