From 439e718206f2599a83574c2197c1d5312d36d2c9 Mon Sep 17 00:00:00 2001 From: Timothy Kim Date: Sat, 18 Apr 2026 08:23:09 -0400 Subject: [PATCH] add user/samba restore script + yireh and isaac smb shares --- RUNBOOK.md | 65 +++++++++++++++++-- scripts/backup.sh | 5 ++ scripts/restore-users.sh | 137 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 200 insertions(+), 7 deletions(-) create mode 100755 scripts/restore-users.sh diff --git a/RUNBOOK.md b/RUNBOOK.md index 3629a0a..a12ac94 100644 --- a/RUNBOOK.md +++ b/RUNBOOK.md @@ -75,7 +75,7 @@ git clone /mnt/backup/argento/docker/gitea/data/git/repositories/timothykim/arge Clone first -- ZFS datasets will mount into this directory tree in the next step. -## 7. ZFS pools + data restore +## 7. ZFS pools **If ZFS disks are intact (OS drive failure only)** -- the common case: @@ -88,15 +88,12 @@ zpool import threeteras # Verify mountpoints (update if the old /opt/docker/ paths are still set): zfs set mountpoint=/opt/argento/docker/nextcloud/data nextcloud/data zfs set mountpoint=/opt/argento/smb/media threeteras/media +zfs set mountpoint=/opt/argento/smb/yireh threeteras/yireh +zfs set mountpoint=/opt/argento/smb/isaac threeteras/isaac zfs set mountpoint=/opt/argento/docker/garage/data threeteras/garage zfs set mountpoint=/opt/argento/docker/immich/library threeteras/immich zpool status # verify pools are ONLINE - -# Restore non-ZFS app data (Gitea, Minecraft, Jellyfin config) from USB: -rsync -a --exclude='docker/nextcloud/data/' --exclude='docker/garage/data/' \ - --exclude='docker/immich/library/' --exclude='smb/' \ - /mnt/backup/argento/ /opt/argento/ ``` **If ZFS disks are lost (full rebuild):** @@ -116,12 +113,66 @@ zfs create -o mountpoint=/opt/argento/docker/nextcloud/data nextcloud/data zpool create threeteras raidz2 /dev/disk/by-id/ ... /dev/disk/by-id/ zfs set compression=lz4 threeteras zfs create -o mountpoint=/opt/argento/smb/media threeteras/media +zfs create -o mountpoint=/opt/argento/smb/yireh -o quota=1T threeteras/yireh +zfs create -o mountpoint=/opt/argento/smb/isaac -o quota=1T threeteras/isaac zfs create -o mountpoint=/opt/argento/docker/garage/data threeteras/garage zfs create -o mountpoint=/opt/argento/docker/immich/library threeteras/immich zpool status # verify pools are ONLINE -# Restore everything from USB (ZFS mountpoints are set, data lands in the right place): +# Per-user SMB share dirs need ownership + setgid (rsync in step 7c will +# preserve these if a USB backup exists, but freshly-created datasets are +# root-owned and need this fixup). +for u in yireh isaac; do + chown "$u":sambagroup "/opt/argento/smb/$u" + chmod 2770 "/opt/argento/smb/$u" +done +``` + +## 7b. Restore users + +System users and the samba passdb are not part of the OS install or the +`/opt/argento/` rsync. Recreate them now, BEFORE the data rsync in step 7c, +so file ownership lands on the correct UIDs. + +```bash +/opt/argento/scripts/restore-users.sh +``` + +This recreates timothykim, yireh, isaac, and the sambagroup group with +their original UIDs/GIDs, then restores the samba password hashes from +`/mnt/backup/samba-private/` if present. + +If the samba passdb backup is missing, set passwords manually: + +```bash +for u in timothykim yireh isaac; do + smbpasswd -a "$u" + smbpasswd -e "$u" +done +``` + +Set timothykim's Linux login password (not in any backup): + +```bash +passwd timothykim +``` + +## 7c. Restore data from USB + +**If ZFS disks were intact:** + +```bash +# Skip the big ZFS-backed dirs — that data is already on disk +rsync -a --exclude='docker/nextcloud/data/' --exclude='docker/garage/data/' \ + --exclude='docker/immich/library/' --exclude='smb/' \ + /mnt/backup/argento/ /opt/argento/ +``` + +**If ZFS was rebuilt from scratch:** + +```bash +# Restore everything (ZFS mountpoints are set, data lands in the right place) rsync -a /mnt/backup/argento/ /opt/argento/ ``` diff --git a/scripts/backup.sh b/scripts/backup.sh index 1215992..a1d073d 100755 --- a/scripts/backup.sh +++ b/scripts/backup.sh @@ -46,6 +46,11 @@ RSYNC_OPTS="-a --delete" rsync $RSYNC_OPTS /opt/argento/ /mnt/backup/argento/ \ || FAILURES+=("rsync argento") +# Back up samba passdb (binary — holds SMB password hashes, not in /opt/argento) +mkdir -p /mnt/backup/samba-private +rsync -a --delete /var/lib/samba/private/ /mnt/backup/samba-private/ \ + || FAILURES+=("rsync samba passdb") + if [ ${#FAILURES[@]} -gt 0 ]; then echo "BACKUP FAILED: ${FAILURES[*]}" >&2 exit 1 diff --git a/scripts/restore-users.sh b/scripts/restore-users.sh new file mode 100755 index 0000000..9b1d3be --- /dev/null +++ b/scripts/restore-users.sh @@ -0,0 +1,137 @@ +#!/bin/bash +# Recreate human users + groups with stable UIDs/GIDs, and restore the +# samba passdb from USB backup. Run during recovery BEFORE the data rsync +# so file ownership lands on the right UIDs. +# +# UIDs/GIDs MUST match what the original system used, or ZFS files end up +# owned by orphan numeric IDs. The script aborts loudly if it finds an +# existing user/group with a different UID/GID than expected — investigate +# and either rename the conflicting entry or update the constants below. +# +# Verify on a live system with: +# id timothykim # check uid + primary gid +# getent group sambagroup # check sambagroup gid +set -euo pipefail + +[ "$EUID" -eq 0 ] || { echo "must run as root" >&2; exit 1; } + +SAMBAGROUP_GID=1001 + +# Format: username:uid:gid:shell:home_flag:supp_groups +# - gid: numeric gid for the user's primary group (group name = username) +# - home_flag: "home" creates /home/, "nohome" skips it (samba-only) +# - supp_groups: comma-separated supplementary groups, or "-" for none +USERS=( + "timothykim:1000:1000:/bin/bash:home:wheel,video,docker,sambagroup" + "yireh:1100:1100:/sbin/nologin:nohome:sambagroup" + "isaac:1101:1101:/sbin/nologin:nohome:sambagroup" +) + +# Abort if an existing entity's numeric ID doesn't match what we expect +assert_match() { + local label=$1 expected=$2 actual=$3 + if [ "$expected" != "$actual" ]; then + echo "[FATAL] $label mismatch: expected $expected, got $actual" >&2 + echo " ZFS file ownership will break if you proceed." >&2 + echo " Either rename the conflicting entry or update this script." >&2 + exit 1 + fi +} + +echo "=== sambagroup ===" +if getent group sambagroup &>/dev/null; then + actual=$(getent group sambagroup | cut -d: -f3) + assert_match "sambagroup gid" "$SAMBAGROUP_GID" "$actual" + echo "[ok] sambagroup exists (gid $actual)" +else + groupadd -g "$SAMBAGROUP_GID" sambagroup + echo "[+] created sambagroup (gid $SAMBAGROUP_GID)" +fi + +# Preflight: every supplementary group referenced must already exist. +# wheel/video are OS-default; docker is created by docker-ce install (RUNBOOK +# step 2); sambagroup was just handled above. If anything's missing, abort +# before touching users — half-applied group memberships are annoying to undo. +echo "" +echo "=== preflight: supplementary groups ===" +missing=() +for entry in "${USERS[@]}"; do + supp=$(echo "$entry" | cut -d: -f6) + [ "$supp" = "-" ] && continue + IFS=, read -ra groups <<< "$supp" + for g in "${groups[@]}"; do + if ! getent group "$g" &>/dev/null; then + missing+=("$g") + fi + done +done +if [ ${#missing[@]} -gt 0 ]; then + # de-dupe for a cleaner error message + uniq_missing=$(printf '%s\n' "${missing[@]}" | sort -u | tr '\n' ' ') + echo "[FATAL] required supplementary groups missing: $uniq_missing" >&2 + echo " likely cause: a package that creates the group hasn't been" >&2 + echo " installed yet (e.g., docker-ce creates the docker group)." >&2 + echo " complete RUNBOOK step 2 before running this script." >&2 + exit 1 +fi +echo "[ok] all supplementary groups present" + +for entry in "${USERS[@]}"; do + IFS=: read -r u uid gid shell home_flag supp <<< "$entry" + echo "" + echo "=== $u ===" + + # Primary group (same name as user, pinned GID — pre-created so useradd + # doesn't auto-assign one via USERGROUPS_ENAB) + if getent group "$u" &>/dev/null; then + actual=$(getent group "$u" | cut -d: -f3) + assert_match "$u primary group gid" "$gid" "$actual" + echo "[ok] group $u exists (gid $actual)" + else + groupadd -g "$gid" "$u" + echo "[+] created group $u (gid $gid)" + fi + + # User account + if id "$u" &>/dev/null; then + actual_uid=$(id -u "$u") + actual_gid=$(id -g "$u") + assert_match "$u uid" "$uid" "$actual_uid" + assert_match "$u primary gid" "$gid" "$actual_gid" + echo "[ok] $u exists (uid $actual_uid, gid $actual_gid)" + else + if [ "$home_flag" = "home" ]; then + useradd -u "$uid" -g "$gid" -s "$shell" -m "$u" + else + useradd -u "$uid" -g "$gid" -s "$shell" -M "$u" + fi + echo "[+] created $u (uid $uid, gid $gid)" + fi + + # Supplementary groups (idempotent — usermod -aG won't error on duplicates) + if [ "$supp" != "-" ]; then + usermod -aG "$supp" "$u" + echo "[+] $u in supplementary groups: $supp" + fi +done + +echo "" +echo "=== samba passdb ===" +if ! mountpoint -q /mnt/backup; then + echo "[!] /mnt/backup is not mounted — skipping samba passdb restore" + echo " mount the USB backup drive (RUNBOOK step 5) and re-run if you want" + echo " to restore preserved SMB passwords; otherwise set them manually:" + echo " for u in timothykim yireh isaac; do smbpasswd -a \"\$u\"; done" +elif [ -f /mnt/backup/samba-private/passdb.tdb ]; then + install -d -m 700 /var/lib/samba/private + cp /mnt/backup/samba-private/*.tdb /var/lib/samba/private/ + chmod 600 /var/lib/samba/private/*.tdb + echo "[+] restored samba passdb from /mnt/backup/samba-private/" +else + echo "[!] no samba passdb backup found at /mnt/backup/samba-private/" + echo " run: for u in timothykim yireh isaac; do smbpasswd -a \"\$u\"; done" +fi + +echo "" +echo "=== Done ===" +echo "Next: set timothykim's Linux login password if not already (passwd timothykim)"