diff --git a/02_setup_ssh.sh b/02_setup_ssh.sh index 7a154cd..bdc63bb 100644 --- a/02_setup_ssh.sh +++ b/02_setup_ssh.sh @@ -7,7 +7,8 @@ CFG="$SCRIPT_DIR/config.sh" # shellcheck disable=SC1090 source "$CFG" -# --- helpers --- +RUN_USER="${SUDO_USER:-$USER}" + APP="raspi-backup" STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/${APP}" LOG_FILE="${STATE_DIR}/${APP}.log" @@ -23,43 +24,50 @@ nas_alias(){ echo "${ALIAS_PREFIX}-$(host_short)"; } need_root need_cmd ssh need_cmd ssh-keygen +need_cmd tee +need_cmd awk +need_cmd mktemp -# validate config vars -: "${NAS_HOST:?}" "${NAS_USER:?}" "${NAS_PORT:?}" "${KEY_TYPE:?}" "${ALIAS_PREFIX:?}" "${SSH_USER:?}" "${NAS_AUTH_KEYS_FILE:?}" +: "${NAS_HOST:?}" "${NAS_USER:?}" "${NAS_PORT:?}" "${KEY_TYPE:?}" "${ALIAS_PREFIX:?}" "${NAS_AUTH_KEYS_FILE:?}" hn="$(host_short)" alias="$(nas_alias)" -ssh_user="$SSH_USER" +ssh_user="$RUN_USER" user_home="$(eval echo "~${ssh_user}")" -[[ -d "$user_home" ]] || die "Home für SSH_USER '$ssh_user' nicht gefunden." +[[ -d "$user_home" ]] || die "Home für User '$ssh_user' nicht gefunden." ssh_dir="${user_home}/.ssh" key="${ssh_dir}/id_${KEY_TYPE}_${hn}" pub="${key}.pub" -cfg="${ssh_dir}/config" +ssh_cfg="${ssh_dir}/config" -log "SSH Setup START: ssh_user=${ssh_user} alias=${alias} nas=${NAS_USER}@${NAS_HOST}:${NAS_PORT} key=${key}" -log "NAS_AUTH_KEYS_FILE: ${NAS_AUTH_KEYS_FILE}" +log "SSH Setup START" +log "Run user : ${ssh_user}" +log "NAS : ${NAS_USER}@${NAS_HOST}:${NAS_PORT}" +log "Alias : ${alias}" +log "Key : ${key}" -# .ssh anlegen sudo -u "$ssh_user" mkdir -p "$ssh_dir" sudo -u "$ssh_user" chmod 700 "$ssh_dir" -sudo -u "$ssh_user" touch "$cfg" -sudo -u "$ssh_user" chmod 600 "$cfg" +sudo -u "$ssh_user" touch "$ssh_cfg" +sudo -u "$ssh_user" chmod 600 "$ssh_cfg" -# Key anlegen falls fehlt if [[ ! -f "$key" ]]; then - log "Erzeuge Key: $key" + log "Erzeuge SSH Key" sudo -u "$ssh_user" ssh-keygen -t "$KEY_TYPE" -a 64 -f "$key" -N "" -C "${ssh_user}@${hn}" else - log "Key existiert: $key" + log "SSH Key existiert bereits" fi -# Alias Block append-only -if ! sudo -u "$ssh_user" grep -qE "^Host[[:space:]]+${alias}$" "$cfg"; then - log "Füge Alias Block hinzu: $alias" - sudo -u "$ssh_user" tee -a "$cfg" >/dev/null </dev/null 2>&1 || true' EXIT + +# ✅ WICHTIG: awk + Umleitung laufen komplett als Zieluser +sudo -u "$ssh_user" bash -c ' + set -Eeuo pipefail + b="$1"; e="$2"; src="$3"; dst="$4" + awk -v b="$b" -v e="$e" '"'"' + $0==b {skip=1; next} + $0==e {skip=0; next} + skip==0 {print} + '"'"' "$src" > "$dst" +' _ "$BEGIN_MARK" "$END_MARK" "$ssh_cfg" "$tmp_cfg" + +printf "\n%s\n" "$block_content" | sudo -u "$ssh_user" tee -a "$tmp_cfg" >/dev/null +sudo -u "$ssh_user" cat "$tmp_cfg" > "$ssh_cfg" + +log "Lerne NAS Hostkey für User '${ssh_user}' (known_hosts aktualisieren)…" +sudo -u "$ssh_user" ssh \ + -p "$NAS_PORT" \ + -o StrictHostKeyChecking=accept-new \ + -o ConnectTimeout=10 \ + "${NAS_USER}@${NAS_HOST}" "true" >/dev/null \ + || die "Hostkey-Initialisierung (User) fehlgeschlagen" + +log "Lerne NAS Hostkey zusätzlich für root (sauber via accept-new)…" +sudo mkdir -p /root/.ssh +sudo chmod 700 /root/.ssh +sudo ssh \ + -p "$NAS_PORT" \ + -i "$key" \ + -o IdentitiesOnly=yes \ + -o BatchMode=yes \ + -o StrictHostKeyChecking=accept-new \ + -o ConnectTimeout=10 \ + "${NAS_USER}@${NAS_HOST}" "true" >/dev/null \ + || die "Hostkey-Initialisierung (root) fehlgeschlagen" + +log "Installiere Public Key auf NAS" +sudo -u "$ssh_user" ssh -p "$NAS_PORT" "${NAS_USER}@${NAS_HOST}" "set -e +f='${NAS_AUTH_KEYS_FILE}' +d=\$(dirname \"\$f\") +mkdir -p \"\$d\" +touch \"\$f\" +chmod 600 \"\$f\" +" || die "Vorbereitung authorized_keys auf NAS fehlgeschlagen" + +pubkey="$(sudo -u "$ssh_user" cat "$pub")" +if sudo -u "$ssh_user" ssh -p "$NAS_PORT" "${NAS_USER}@${NAS_HOST}" \ + "grep -qxF \"$pubkey\" '${NAS_AUTH_KEYS_FILE}'" >/dev/null 2>&1; then + log "Public Key ist auf NAS schon vorhanden" else - log "Alias existiert schon: $alias" + log "Hänge Public Key an authorized_keys an" + sudo -u "$ssh_user" ssh -p "$NAS_PORT" "${NAS_USER}@${NAS_HOST}" \ + "printf '%s\n' \"$pubkey\" >> '${NAS_AUTH_KEYS_FILE}'" \ + || die "Anhängen des Public Keys fehlgeschlagen" fi -# Testconnect (accept-new nur beim Setup) -log "SSH Verbindungstest (accept-new nur setup)..." -sudo -u "$ssh_user" ssh -p "$NAS_PORT" -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 \ - "${NAS_USER}@${NAS_HOST}" "echo ok" >/dev/null \ - || die "SSH Verbindung fehlgeschlagen: ${NAS_HOST}:${NAS_PORT}" - -# Pubkey lesen -[[ -f "$pub" ]] || die "Public key fehlt: $pub" -pubkey="$(sudo -u "$ssh_user" cat "$pub")" - -# WICHTIG: Key in zentrale NAS-Datei schreiben (nicht ~/.ssh/authorized_keys) -# - legt Verzeichnis an -# - sorgt für Datei + Rechte -# - fügt Key nur hinzu, wenn noch nicht vorhanden -log "Installiere Public Key in zentrale Datei am NAS..." -sudo -u "$ssh_user" ssh -p "$NAS_PORT" -o ConnectTimeout=10 \ - "${NAS_USER}@${NAS_HOST}" \ - "set -e; - f='${NAS_AUTH_KEYS_FILE}'; - d=\$(dirname \"\$f\"); - mkdir -p \"\$d\"; - touch \"\$f\"; - chmod 600 \"\$f\"; - grep -qxF '$pubkey' \"\$f\" || echo '$pubkey' >> \"\$f\"" - -log "SSH Setup OK." -echo "SSH Setup OK." +log "SSH Setup OK" +echo "SSH Setup OK" echo "Test:" echo " sudo -u ${ssh_user} ssh ${alias} 'echo hello'" +echo " sudo ssh -p ${NAS_PORT} -i ${key} -o BatchMode=yes -o StrictHostKeyChecking=yes ${NAS_USER}@${NAS_HOST} 'echo hello'" diff --git a/03_verify.sh b/03_verify.sh index f9f7dbc..ce5e7d0 100644 --- a/03_verify.sh +++ b/03_verify.sh @@ -7,6 +7,9 @@ CFG="$SCRIPT_DIR/config.sh" # shellcheck disable=SC1090 source "$CFG" +# Der User, der das Script gestartet hat (auch wenn via sudo) +RUN_USER="${SUDO_USER:-$USER}" + # --- helpers --- APP="raspi-backup" STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/${APP}" @@ -22,17 +25,20 @@ nas_alias(){ echo "${ALIAS_PREFIX}-$(host_short)"; } need_root need_cmd ssh -need_cmd rsync -# validate config vars -: "${NAS_HOST:?}" "${NAS_USER:?}" "${NAS_PORT:?}" "${ALIAS_PREFIX:?}" "${SSH_USER:?}" "${NAS_BACKUP_BASE:?}" +# validate config vars (OHNE SSH_USER) +: "${NAS_HOST:?}" "${NAS_USER:?}" "${NAS_PORT:?}" "${ALIAS_PREFIX:?}" "${NAS_BACKUP_BASE:?}" "${KEY_TYPE:?}" hn="$(host_short)" alias="$(nas_alias)" -ssh_user="$SSH_USER" +ssh_user="$RUN_USER" remote_root="${NAS_BACKUP_BASE%/}/${hn}" -log "VERIFY START: alias=${alias} remote_root=${remote_root}" +# Keypfad (gehört RUN_USER) +key_path="$(eval echo "~${ssh_user}/.ssh/id_${KEY_TYPE}_${hn}")" +[[ -f "$key_path" ]] || die "Key fehlt: $key_path (erst 02_setup_ssh.sh ausführen)" + +log "VERIFY START: run_user=${ssh_user} alias=${alias} remote_root=${remote_root}" # optional Port check if command -v nc >/dev/null 2>&1; then @@ -42,18 +48,39 @@ else log "nc nicht vorhanden – überspringe Port-Check" fi -# SSH login via alias -log "Check SSH Login: ${alias}" -sudo -u "$ssh_user" ssh -o BatchMode=yes -o ConnectTimeout=10 "$alias" "echo ok" >/dev/null \ - || die "SSH Login über Alias fehlgeschlagen: $alias" +# Direkte SSH Optionen (robust, nicht abhängig von ssh-config/alias) +SSH_BASE=(ssh -p "$NAS_PORT" -i "$key_path" + -o IdentitiesOnly=yes + -o BatchMode=yes + -o StrictHostKeyChecking=yes + -o ConnectTimeout=10 +) -# Remote backup dir +# 1) SSH login test als RUN_USER (wie bisher) +log "Check SSH Login (RUN_USER): ${NAS_USER}@${NAS_HOST}:${NAS_PORT}" +if ! sudo -u "$ssh_user" "${SSH_BASE[@]}" "${NAS_USER}@${NAS_HOST}" "echo ok" >/dev/null 2>&1; then + die "SSH Login fehlgeschlagen als User '${ssh_user}' (Hostkey/Key/Netz prüfen)" +fi + +# 2) Extra: SSH login test als root (wichtig, weil Backup Run via sudo/root) +log "Check SSH Login (root): ${NAS_USER}@${NAS_HOST}:${NAS_PORT}" +if ! "${SSH_BASE[@]}" "${NAS_USER}@${NAS_HOST}" "echo ok" >/dev/null 2>&1; then + echo "Hinweis: root kann nicht verbinden (oft fehlt root der Hostkey in /root/.ssh/known_hosts)." >&2 + echo "Fix: 02_setup_ssh.sh erneut laufen lassen (lernt Hostkey für root), oder root-known_hosts ergänzen." >&2 + die "SSH Login fehlgeschlagen als root" +fi + +# 3) Remote backup dir log "Check/Create Remote Backup Dir: ${remote_root}" -sudo -u "$ssh_user" ssh -o BatchMode=yes "$alias" "mkdir -p '$remote_root' && test -d '$remote_root'" >/dev/null \ +sudo -u "$ssh_user" "${SSH_BASE[@]}" "${NAS_USER}@${NAS_HOST}" \ + "mkdir -p '$remote_root' && test -d '$remote_root'" >/dev/null \ || die "Remote Backup Pfad nicht nutzbar: $remote_root" log "VERIFY OK" echo "Verify OK:" echo "- NAS erreichbar (${NAS_HOST}:${NAS_PORT})" -echo "- SSH Alias funktioniert (${alias})" +echo "- SSH Login OK (User: ${NAS_USER}@${NAS_HOST}:${NAS_PORT})" +echo "- SSH Login OK (root: ${NAS_USER}@${NAS_HOST}:${NAS_PORT})" +echo "- Key OK (${key_path})" echo "- Remote Backup Pfad OK (${remote_root})" +echo "Info: SSH-Alias (optional) wäre: ${alias}" diff --git a/04_run_backup.sh b/04_run_backup.sh index af5001c..4ba419a 100644 --- a/04_run_backup.sh +++ b/04_run_backup.sh @@ -7,6 +7,9 @@ CFG="$SCRIPT_DIR/config.sh" # shellcheck disable=SC1090 source "$CFG" +# User, der das Script gestartet hat (auch wenn via sudo) +RUN_USER="${SUDO_USER:-$USER}" + # --- helpers --- LOG_FILE="$SCRIPT_DIR/raspi-backup.log" LOCK_FILE="$SCRIPT_DIR/.raspi-backup.lock" @@ -38,13 +41,13 @@ need_cmd rsync need_cmd ssh need_cmd tee -# validate config vars -: "${NAS_HOST:?}" "${NAS_USER:?}" "${NAS_PORT:?}" "${SSH_USER:?}" "${NAS_BACKUP_BASE:?}" "${KEEP_DAYS:?}" "${KEY_TYPE:?}" +# validate config vars (OHNE SSH_USER) +: "${NAS_HOST:?}" "${NAS_USER:?}" "${NAS_PORT:?}" "${NAS_BACKUP_BASE:?}" "${KEEP_DAYS:?}" "${KEY_TYPE:?}" hn="$(host_short)" -ssh_user="$SSH_USER" +ssh_user="$RUN_USER" -# Keypfad: gehört SSH_USER +# Keypfad: gehört RUN_USER key_path="$(eval echo "~${ssh_user}/.ssh/id_${KEY_TYPE}_${hn}")" [[ -f "$key_path" ]] || die "Key fehlt: $key_path (erst 02_setup_ssh.sh ausführen)" @@ -54,6 +57,7 @@ remote_dest="${remote_root}/${run_ts}" log "============================================================" log "BACKUP START" +log "Run user : ${ssh_user}" log "Host : ${hn}" log "NAS : ${NAS_USER}@${NAS_HOST}:${NAS_PORT}" log "Key (local): ${key_path}" @@ -64,15 +68,23 @@ log "Logfile : ${LOG_FILE}" log "============================================================" acquire_lock - start_epoch="$(date +%s)" -# Common SSH options (no alias dependency) +# ---- Performance / SSH tuning ---- +SSH_CIPHER="chacha20-poly1305@openssh.com" + +SSH_COMPRESS_OPT="-o Compression=no" +if [[ "${BACKUP_SSH_COMPRESS:-no}" == "yes" ]]; then + SSH_COMPRESS_OPT="-o Compression=yes -o CompressionLevel=3" +fi + SSH_BASE=(ssh -p "$NAS_PORT" -i "$key_path" -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=yes -o ConnectTimeout=20 + -o Ciphers="$SSH_CIPHER" + $SSH_COMPRESS_OPT ) log "Phase 1/5: Remote Ordner anlegen..." @@ -91,13 +103,40 @@ if declare -p RSYNC_EXTRA >/dev/null 2>&1; then for o in "${RSYNC_EXTRA[@]}"; do extra_args+=( "$o" ); done fi -ssh_cmd="ssh -p ${NAS_PORT} -i ${key_path} -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=yes -o ConnectTimeout=20" +# ---- Fix: --inplace kollidiert mit --partial-dir / --partial ---- +use_inplace=1 +for o in "${extra_args[@]}"; do + case "$o" in + --partial-dir*|--partial) + use_inplace=0 + ;; + esac +done + +inplace_args=() +if [[ "$use_inplace" -eq 1 ]]; then + inplace_args=(--inplace) + log "rsync: --inplace aktiv" +else + log "rsync: --inplace deaktiviert (Konflikt mit --partial/--partial-dir aus RSYNC_EXTRA)" +fi + +ssh_cmd="ssh -p ${NAS_PORT} -i ${key_path} \ + -o IdentitiesOnly=yes \ + -o BatchMode=yes \ + -o StrictHostKeyChecking=yes \ + -o ConnectTimeout=20 \ + -o Ciphers=${SSH_CIPHER} \ + ${SSH_COMPRESS_OPT} +" log "Phase 2/5: rsync START (Live-Progress sichtbar)..." log "Hinweis: Das kann je nach Datenmenge sehr lange dauern." -# WICHTIG: --omit-dir-times verhindert, dass der frische Backup-Ordner durch alte mtime sofort von Retention gelöscht wird + rsync -aAXH --numeric-ids \ --omit-dir-times \ + --no-inc-recursive \ + "${inplace_args[@]}" \ --info=progress2,stats2 \ "${exclude_args[@]}" \ "${extra_args[@]}" \ diff --git a/05_status.sh b/05_status.sh index 6849e4f..256f760 100644 --- a/05_status.sh +++ b/05_status.sh @@ -7,17 +7,24 @@ CFG="$SCRIPT_DIR/config.sh" # shellcheck disable=SC1090 source "$CFG" +# User, der das Script gestartet hat (auch wenn via sudo) +RUN_USER="${SUDO_USER:-$USER}" + LOG_FILE="$SCRIPT_DIR/raspi-backup.log" host_short(){ hostname -s 2>/dev/null || hostname 2>/dev/null || echo "raspi"; } nas_alias(){ echo "${ALIAS_PREFIX}-$(host_short)"; } +# validate config vars (OHNE SSH_USER) +: "${NAS_HOST:?}" "${NAS_USER:?}" "${NAS_PORT:?}" "${NAS_BACKUP_BASE:?}" "${KEY_TYPE:?}" "${ALIAS_PREFIX:?}" + hn="$(host_short)" alias="$(nas_alias)" remote_root="${NAS_BACKUP_BASE%/}/${hn}" echo "== Backup Status ==" echo "Script-Ordner : $SCRIPT_DIR" +echo "Run user : $RUN_USER" echo "Host : $hn" echo "NAS : ${NAS_USER}@${NAS_HOST}:${NAS_PORT}" echo "Remote Root : $remote_root" @@ -27,17 +34,8 @@ echo # Log-Info (letzter Lauf) if [[ -f "$LOG_FILE" ]]; then echo "-- Letzter Log-Block (kurz) --" - # Zeige die letzten ~25 Zeilen, aber etwas "smart" ab BACKUP START wenn vorhanden if grep -q "BACKUP START" "$LOG_FILE"; then - awk ' - BEGIN{start=0} - /BACKUP START/{start=1} - {buf[NR]=$0} - END{ - # gib die letzten 80 Zeilen aus, aber nur wenn start irgendwann vorkam - from = (NR-80>1)?NR-80:1 - for(i=from;i<=NR;i++) print buf[i] - }' "$LOG_FILE" | tail -n 25 + tail -n 120 "$LOG_FILE" | tail -n 25 else tail -n 25 "$LOG_FILE" fi @@ -47,15 +45,13 @@ else echo fi -# NAS Status (ohne Passwort möglich nur wenn Key-Auth passt) echo "-- NAS Check --" echo "Alias (nur Info): $alias" -echo "Hinweis: Dieser Status nutzt direkten SSH Host/Port/User. Falls Passwortabfrage kommt, ist Key-Auth noch nicht sauber." +echo "Hinweis: Status nutzt direkten SSH Host/Port/User/Key." echo -# Keypfad für SSH_USER -hn2="$hn" -key_path="$(eval echo "~${SSH_USER}/.ssh/id_${KEY_TYPE}_${hn2}")" +# Keypfad für RUN_USER +key_path="$(eval echo "~${RUN_USER}/.ssh/id_${KEY_TYPE}_${hn}")" if [[ ! -f "$key_path" ]]; then echo "Local Key fehlt: $key_path" @@ -71,16 +67,16 @@ SSH_BASE=(ssh -p "$NAS_PORT" -i "$key_path" ) # Remote listing -if sudo -u "$SSH_USER" "${SSH_BASE[@]}" "${NAS_USER}@${NAS_HOST}" "test -d '$remote_root'" >/dev/null 2>&1; then +if sudo -u "$RUN_USER" "${SSH_BASE[@]}" "${NAS_USER}@${NAS_HOST}" "test -d '$remote_root'" >/dev/null 2>&1; then echo "Remote erreichbar: OK" echo echo "-- Remote Inhalt --" - sudo -u "$SSH_USER" "${SSH_BASE[@]}" "${NAS_USER}@${NAS_HOST}" "ls -la '$remote_root' | sed -n '1,40p'" + sudo -u "$RUN_USER" "${SSH_BASE[@]}" "${NAS_USER}@${NAS_HOST}" "ls -la '$remote_root' | sed -n '1,40p'" echo echo "-- latest Ziel + Größe (wenn vorhanden) --" - sudo -u "$SSH_USER" "${SSH_BASE[@]}" "${NAS_USER}@${NAS_HOST}" \ + sudo -u "$RUN_USER" "${SSH_BASE[@]}" "${NAS_USER}@${NAS_HOST}" \ "if [ -L '$remote_root/latest' ]; then \ echo -n 'latest -> '; readlink '$remote_root/latest' || true; \ echo -n 'Size(latest): '; du -sh '$remote_root/latest' 2>/dev/null || echo 'n/a'; \