minecraft-server/scripts/restic-init.sh
s8n 3336f52142 redact: scrub leaked Minecraft secrets from public repo
Replaced literal values with env-var placeholders (${RCON_PASSWORD},
${MGMT_SECRET}, ${MC_RCON_PASSWORD}) across server.properties,
.rcon-cli.env, docker-compose.yml(s), backup scripts, and AUDIT-2026-05-07.md.

Affected secrets:
- Paper management-server-secret (HIGH; mitigated by management-server-enabled=false)
- RCON password '*redacted*' (MEDIUM; bound to 127.0.0.1)
- MC_RCON_PASSWORD backup-pipeline default fallback (MEDIUM; same blast radius)

WARNING: HEAD redaction only — values remain in git history. Treat as
compromised and rotate (closes F-17 audit-finding's deferred TODO).
Originals backed up to private s8n/secrets/minecraft-server/.
2026-05-08 15:36:20 +01:00

156 lines
5.5 KiB
Bash

#!/usr/bin/env bash
# scripts/restic-init.sh
#
# One-time bootstrap for the Phase 2 restic backup chain. Run this on
# nullstone as root (sudo) AFTER `apt install restic mcrcon`.
#
# What it does:
# 1. Generates /etc/mc-backup.pw (40-byte random restic password) if absent.
# 2. Writes /etc/mc-backup.env (consumed by restic-backup-playerdata.sh).
# 3. Initialises the local restic repo at /home/user/restic/mc-frequent.
# 4. Takes a baseline snapshot so the timer's first run is fast.
# 5. Optionally adds an SFTP-mirror block once onyx is provisioned.
#
# Idempotent — re-running is safe; existing files are preserved.
#
# Cross-ref: docs/BACKUP-STRATEGY.md §8.2, docs/RUNBOOK-BACKUP-RESTORE.md.
set -euo pipefail
umask 077
if [ "$(id -u)" -ne 0 ]; then
echo "FATAL: must run as root (sudo)." >&2
exit 2
fi
if ! command -v restic >/dev/null 2>&1; then
echo "FATAL: restic not installed. Run: apt install restic mcrcon" >&2
exit 3
fi
# Resolve target user — restic repo lives under their home so /opt
# disk pressure doesn't matter. nullstone: 142G free on /home.
TARGET_USER="${TARGET_USER:-user}"
if ! id "$TARGET_USER" >/dev/null 2>&1; then
echo "FATAL: user '$TARGET_USER' not found" >&2
exit 4
fi
TARGET_HOME=$(getent passwd "$TARGET_USER" | cut -d: -f6)
PW_FILE="/etc/mc-backup.pw"
ENV_FILE="/etc/mc-backup.env"
REPO_FREQUENT="${TARGET_HOME}/restic/mc-frequent"
REPO_WORLD="${TARGET_HOME}/restic/mc-world"
LOG_DIR="/var/log"
SENTINEL_DIR="/var/lib/mc-backup"
# 1. Password file
if [ ! -e "$PW_FILE" ]; then
head -c 40 /dev/urandom | base64 > "$PW_FILE"
chown root:root "$PW_FILE"
chmod 600 "$PW_FILE"
echo "Generated $PW_FILE (40 bytes random)."
else
echo "$PW_FILE already exists — keeping."
fi
# 2. Env file (only created if missing; user can edit afterwards).
if [ ! -e "$ENV_FILE" ]; then
cat > "$ENV_FILE" <<EOF
# /etc/mc-backup.env — consumed by restic-backup-playerdata.sh and the
# Class B/C/D world script (TBD). Edit as needed.
RESTIC_REPOSITORY_FREQUENT=$REPO_FREQUENT
RESTIC_REPOSITORY_WORLD=$REPO_WORLD
RESTIC_PASSWORD_FILE=$PW_FILE
MC_DATA=/opt/docker/minecraft
MC_BACKUP_LOG=$LOG_DIR/mc-backup.log
MC_BACKUP_FREQUENT_SENTINEL=$SENTINEL_DIR/last-success-frequent
MC_BACKUP_WORLD_SENTINEL=$SENTINEL_DIR/last-success-world
# RCON — used to flush MC saves before snapshotting. Pull from the live
# compose file (services.mc.environment.RCON_PASSWORD).
MC_RCON_HOST=127.0.0.1
MC_RCON_PORT=25575
MC_RCON_PASSWORD=${MC_RCON_PASSWORD}
# Off-host mirror destination (onyx via Tailscale). Empty = skip mirror.
TS_OFFHOST_USER=mc-backup
TS_OFFHOST_HOST=100.64.0.1
TS_OFFHOST_PATH=/backups/nullstone-mc-restic
# Alerting — fill in once ntfy.s8n.ru is up. Leave blank for now.
HEARTBEAT_URL=
ALERT_URL=
EOF
chown root:"$(id -gn "$TARGET_USER")" "$ENV_FILE"
chmod 640 "$ENV_FILE"
echo "Wrote $ENV_FILE (mode 640, group=$(id -gn "$TARGET_USER"))."
else
echo "$ENV_FILE already exists — keeping."
fi
# 3. Log + sentinel dirs (writable by target user).
mkdir -p "$SENTINEL_DIR"
chown "$TARGET_USER":"$(id -gn "$TARGET_USER")" "$SENTINEL_DIR"
chmod 750 "$SENTINEL_DIR"
touch "$LOG_DIR/mc-backup.log"
chown "$TARGET_USER":adm "$LOG_DIR/mc-backup.log" 2>/dev/null \
|| chown "$TARGET_USER":"$(id -gn "$TARGET_USER")" "$LOG_DIR/mc-backup.log"
chmod 640 "$LOG_DIR/mc-backup.log"
# 4. Repo init (idempotent — restic init exits non-zero if repo exists).
init_repo() {
local repo=$1
install -d -o "$TARGET_USER" -g "$(id -gn "$TARGET_USER")" -m 700 \
"$(dirname "$repo")" "$repo"
if RESTIC_PASSWORD_FILE="$PW_FILE" RESTIC_REPOSITORY="$repo" \
runuser -u "$TARGET_USER" -- restic snapshots >/dev/null 2>&1; then
echo "Repo $repo: already initialised."
else
RESTIC_PASSWORD_FILE="$PW_FILE" RESTIC_REPOSITORY="$repo" \
runuser -u "$TARGET_USER" -- restic init
echo "Repo $repo: initialised."
fi
}
init_repo "$REPO_FREQUENT"
init_repo "$REPO_WORLD"
# 5. Baseline snapshot of the frequent repo so the first timer run is fast.
echo "Taking baseline snapshot into $REPO_FREQUENT ..."
runuser -u "$TARGET_USER" -- env \
RESTIC_PASSWORD_FILE="$PW_FILE" \
RESTIC_REPOSITORY="$REPO_FREQUENT" \
restic backup \
--tag playerdata --tag baseline --host "$(hostname)" \
--exclude='*.lock' --exclude='*.tmp' \
/opt/docker/minecraft/world/playerdata \
/opt/docker/minecraft/world/stats \
/opt/docker/minecraft/world/advancements \
/opt/docker/minecraft/homestead_data.db \
/opt/docker/minecraft/plugins/AuthMe \
/opt/docker/minecraft/plugins/CoreProtect/database.db \
/opt/docker/minecraft/plugins/LuckPerms \
|| echo "Baseline snapshot returned non-zero — review output above."
cat <<'NEXT'
---------------------------------------------------------------
restic-init.sh complete.
Next steps:
1. Install systemd units:
install -m644 scripts/systemd/mc-backup-playerdata.service \
/etc/systemd/system/
install -m644 scripts/systemd/mc-backup-playerdata.timer \
/etc/systemd/system/
install -m755 scripts/restic-backup-playerdata.sh \
/usr/local/bin/
2. systemctl daemon-reload
3. systemctl enable --now mc-backup-playerdata.timer
4. Tail: journalctl -u mc-backup-playerdata.service -f
Onyx (off-host mirror) provisioning is a separate step — see
docs/RUNBOOK-BACKUP-RESTORE.md "Phase 2 deployment".
---------------------------------------------------------------
NEXT