3831c85285
Host provisioning for the single-server production target: SSH + firewall hardening (nftables allowlist), k3s node registration, bare-metal Stalwart install with systemd units and TLS cert-sync from the cluster secret, and Restic encrypted backup/restore (primary + DR) with timer units. Host-specific secrets live in config.env (gitignored); config.env.example is the template. Also gitignores MemPalace per-project files.
116 lines
5.2 KiB
Bash
Executable File
116 lines
5.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Install Dezky host backups: Restic + a dedicated backup SSH key/config for the
|
|
# Hetzner Storage Box(es), the env file, the backup/restore scripts, and the
|
|
# nightly systemd timer. Idempotent.
|
|
#
|
|
# sudo ./install.sh
|
|
#
|
|
# Storage Box uses SSH/SFTP on PORT 23 with key auth. After this runs, you must
|
|
# upload the printed public key to BOTH Storage Boxes, then re-run to init the
|
|
# repos (the box must trust the key before `restic init` can connect).
|
|
|
|
set -euo pipefail
|
|
|
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
|
|
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
|
|
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
|
|
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
|
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
HOST_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
CONFIG_FILE="${CONFIG_FILE:-$HOST_DIR/config.env}"
|
|
BACKUP_HOME="/opt/dezky-backup"
|
|
SSH_DIR="$BACKUP_HOME/.ssh"
|
|
KEY="$SSH_DIR/id_ed25519"
|
|
|
|
if [[ $EUID -ne 0 ]]; then error "Run as root."; exit 1; fi
|
|
if [[ ! -f "$CONFIG_FILE" ]]; then error "Missing $CONFIG_FILE"; exit 1; fi
|
|
# shellcheck disable=SC1090
|
|
source "$CONFIG_FILE"
|
|
|
|
: "${RESTIC_PASSWORD:?RESTIC_PASSWORD required (and STORE IT OFFLINE — losing it loses the backups)}"
|
|
: "${BACKUP_PRIMARY_REPO:?BACKUP_PRIMARY_REPO required}"
|
|
: "${BACKUP_PATHS:?BACKUP_PATHS required}"
|
|
: "${BACKUP_RETENTION:=--keep-daily 7 --keep-weekly 4 --keep-monthly 6}"
|
|
|
|
# ── 1) Packages ────────────────────────────────────────────────────────────
|
|
info "Installing restic + openssh client..."
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
apt-get update -qq
|
|
apt-get install -y -qq restic curl openssh-client >/dev/null
|
|
ok "restic $(restic version | awk '{print $2}') installed."
|
|
|
|
# ── 2) Backup home + SSH key/config ────────────────────────────────────────
|
|
info "Setting up $BACKUP_HOME ..."
|
|
install -d -m 0700 "$BACKUP_HOME" "$SSH_DIR"
|
|
if [[ ! -f "$KEY" ]]; then
|
|
ssh-keygen -t ed25519 -N "" -C "dezky-backup@node1" -f "$KEY" >/dev/null
|
|
ok "Generated backup SSH key."
|
|
fi
|
|
# Single wildcard config covers BOTH Storage Boxes (same domain, port 23, key).
|
|
cat > "$SSH_DIR/config" <<EOF
|
|
Host *.your-storagebox.de
|
|
Port 23
|
|
IdentityFile $KEY
|
|
IdentitiesOnly yes
|
|
StrictHostKeyChecking accept-new
|
|
UserKnownHostsFile $SSH_DIR/known_hosts
|
|
EOF
|
|
chmod 0600 "$SSH_DIR/config" "$KEY"
|
|
chmod 0644 "$KEY.pub"
|
|
|
|
# ── 3) restic.env (secrets; generated, not in git) ─────────────────────────
|
|
umask 077
|
|
cat > "$BACKUP_HOME/restic.env" <<EOF
|
|
# Generated by restic/install.sh from config.env — DO NOT commit.
|
|
RESTIC_PASSWORD=${RESTIC_PASSWORD}
|
|
BACKUP_PRIMARY_REPO=${BACKUP_PRIMARY_REPO}
|
|
BACKUP_DR_REPO=${BACKUP_DR_REPO:-}
|
|
BACKUP_PATHS=${BACKUP_PATHS}
|
|
BACKUP_RETENTION=${BACKUP_RETENTION}
|
|
BACKUP_HEALTHCHECK_URL=${BACKUP_HEALTHCHECK_URL:-}
|
|
EOF
|
|
chmod 0600 "$BACKUP_HOME/restic.env"
|
|
ok "Wrote restic.env."
|
|
|
|
# ── 4) Scripts + systemd units ─────────────────────────────────────────────
|
|
install -m 0750 "$SCRIPT_DIR/backup.sh" "$BACKUP_HOME/backup.sh"
|
|
install -m 0750 "$SCRIPT_DIR/restore.sh" "$BACKUP_HOME/restore.sh"
|
|
install -m 0644 "$SCRIPT_DIR/dezky-backup.service" /etc/systemd/system/dezky-backup.service
|
|
install -m 0644 "$SCRIPT_DIR/dezky-backup.timer" /etc/systemd/system/dezky-backup.timer
|
|
systemctl daemon-reload
|
|
systemctl enable --now dezky-backup.timer
|
|
ok "Nightly timer enabled."
|
|
|
|
# ── 5) Try to init the repos (only works once the key is on the box) ───────
|
|
export HOME="$BACKUP_HOME" RESTIC_PASSWORD
|
|
init_repo() {
|
|
local repo="$1" label="$2"
|
|
[[ -z "$repo" ]] && return 0
|
|
if restic -r "$repo" cat config >/dev/null 2>&1; then
|
|
ok "$label repo already initialized."
|
|
elif restic -r "$repo" init >/dev/null 2>&1; then
|
|
ok "$label repo initialized."
|
|
else
|
|
warn "$label repo not reachable/authorized yet — upload the key, then re-run."
|
|
fi
|
|
}
|
|
init_repo "$BACKUP_PRIMARY_REPO" "Primary"
|
|
init_repo "${BACKUP_DR_REPO:-}" "DR"
|
|
|
|
echo ""
|
|
echo "╔══════════════════════════════════════════════════════════════╗"
|
|
echo "║ Backup install complete ║"
|
|
echo "╚══════════════════════════════════════════════════════════════╝"
|
|
warn "Upload this PUBLIC key to BOTH Storage Boxes, then re-run install.sh:"
|
|
echo ""
|
|
cat "$KEY.pub"
|
|
echo ""
|
|
info " ssh-copy-id -p 23 -i $KEY.pub <primary-user>@<primary-host>.your-storagebox.de"
|
|
info " ssh-copy-id -p 23 -i $KEY.pub <dr-user>@<dr-host>.your-storagebox.de"
|
|
info "Then test: sudo $BACKUP_HOME/backup.sh (or wait for 03:20 UTC)"
|
|
info "Drill restore: sudo $BACKUP_HOME/restore.sh restore latest /tmp/restore-test"
|
|
warn "STORE RESTIC_PASSWORD OFFLINE. Without it, the encrypted backups are unrecoverable."
|