From f9e0f5221faa719d9617fe6807e0171b76af65e0 Mon Sep 17 00:00:00 2001 From: Jens Ahrensfeld Date: Sun, 26 Jul 2026 16:29:32 +0200 Subject: [PATCH] Build the alpha -> vlda-01 Gitea backup strategy Implements the sketch from SYNC-PLAN.md item 2b. gitea dump (not raw rsync, to avoid grabbing the live SQLite DB mid-write) runs nightly on alpha via cron, into a dedicated read-only-chrooted SFTP account (giteabackup) reusing the existing ftp_jens/ftp_alex sshd pattern. vlda-01 pulls whatever's new via a User Scripts plugin entry (not a raw crontab - Unraid's / and /usr are RAM-backed and don't survive reboot, only /boot does), fetching only files it doesn't already have so a run after days offline catches up on everything missed. Found and fixed two real issues along the way: a default ACL on the backup directory doesn't actually grant read access to new dump files, since gitea dump creates them 600 and the resulting ACL mask neuters the inherited grant - fixed by applying the ACL explicitly per file. And the restricted account's ForceCommand internal-sftp blocks a real rsync invocation, so the transport is SFTP get, not rsync. Verified end-to-end through the actual scheduled-invocation path on both ends, including a second run correctly skipping an already-pulled file instead of re-fetching it. Co-Authored-By: Claude Sonnet 5 --- README.md | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++ SYNC-PLAN.md | 61 +++++------------------------------------ 2 files changed, 84 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 4ab970b..e52d77f 100644 --- a/README.md +++ b/README.md @@ -352,6 +352,83 @@ Internet → Apache :80/:443 (git.jayfield.org vhost) Gitea's automatic redirect) return `200`, and this repo's local `origin` remote and stored git credentials were updated to match. +### Gitea backup to `vlda-01` (`SYNC-PLAN.md` item 2b) + +Built 2026-07-26. A point-in-time backup, not a hot standby/live mirror — +now that alpha is the canonical live Gitea, this keeps `vlda-01`'s copy +from just going stale after the migration. Same network direction as +everything else in this project: alpha has no route into the home LAN, +so the pull is **initiated by `vlda-01`**, never alpha reaching in. + +**Why `gitea dump`, not a raw `rsync` of `/srv/gitea/data`**: the DB is a +live SQLite file; copying it while Gitea is writing risks shipping a torn +copy. `gitea dump` produces one consistent archive (DB + repos + LFS + +config together) — also what Gitea's own restore procedure expects. + +**On alpha** — `/usr/local/sbin/gitea-backup.sh`, `/etc/cron.d/gitea-backup` +(daily `02:30`, clear of logrotate's midnight timer): +- `docker exec -u git gitea gitea dump -f /data/backups/gitea-dump-.zip --skip-index` + into `/srv/gitea/data/backups/` (inside the existing bind mount — already + host-inspectable, no extra volume). Timestamped to the second, not just + the date: `gitea dump` refuses to overwrite an existing filename, which + a plain per-day name would hit on any manual re-run (found this the hard + way testing it). +- Prunes to the last 14 archives. +- Applies a POSIX ACL (`setfacl -m u:giteabackup:r`) to each new archive + so the pull account below can read it. **Has to happen explicitly per + file** — a default ACL on the directory alone isn't enough: `gitea dump` + creates each file `600`, and the resulting ACL mask neuters the + inherited grant (`getfacl` shows `#effective:---` if this step is + skipped). + +**Transport — a dedicated, restricted account on alpha**, not a shared or +personal credential: +- System account `giteabackup` (`/usr/sbin/nologin`), member of the + existing `sftp` group — reuses the `Match Group sftp` / + `ForceCommand internal-sftp` block already in `sshd_config` for + `ftp_jens`/`ftp_alex`; no new sshd config needed. +- Chrooted to `/var/sftp/giteabackup/`, containing only a **read-only bind + mount** of `/srv/gitea/data/backups` (`/etc/fstab`, `bind,ro`) — even a + misused key can only ever read backup archives, never write, delete, or + reach anything else on the host. Verified: `touch` inside the mount + fails with "Read-only file system" even as root. +- Key-only auth: a dedicated ed25519 keypair generated on `vlda-01` + (private key never leaves it), `authorized_keys` further restricted + (`restrict,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty`) + on top of the chroot/`ForceCommand`. +- **Plain SFTP, not real `rsync`**: `ForceCommand internal-sftp` blocks a + genuine `rsync --server` invocation from the client, so the transport + is SFTP `get`, not rsync. + +**On `vlda-01`** — via the **User Scripts** plugin, not a raw +crontab/`/usr/local/sbin` script: Unraid's `/` and `/usr` are RAM-backed +(`rootfs`/`overlay`) and don't survive a reboot — only `/boot` (the actual +USB stick) does. The pull script lives at +`/boot/config/plugins/user.scripts/scripts/gitea-backup-pull/script`, +scheduled daily `03:30` via `schedule.json`/`customSchedule.cron` +(`update_cron` folds it into the live `/etc/cron.d/root` alongside +Unraid's own generated jobs) — the same plugin already used for this +box's other scheduled maintenance, no new persistence mechanism invented. +The SSH keypair needed no special handling: Unraid already auto-mirrors +`/root/.ssh` to `/boot/config/ssh/root/` on its own. +- Lists what's actually on alpha via SFTP, fetches only filenames not + already present locally — a run after several days offline catches up + on all of them, not just the newest, without re-downloading anything + already pulled. +- Lands in `/mnt/user/appdata/gitea-backups-from-alpha/` (real array + storage, not the RAM-backed root), keeps the last 30 locally — deeper + history than alpha's own 14, given the Unraid array's ample headroom. + +**Restore path, if ever needed**: manual and deliberate — stop the target +Gitea container, unpack the chosen dump's DB/repos/etc. into `/data` per +Gitea's documented restore steps, restart, verify. Not automated. + +**Verified end-to-end**: a real archive produced on alpha, pulled to +`vlda-01` via the actual scheduled-invocation code path (`startCustom.php`, +not just the raw script) and confirmed as a genuinely new file; a second +run correctly reported "up to date, nothing new to pull" rather than +re-fetching. + ### Password vault sync (KeePass, via Nextcloud) Each user's KeePass database now syncs through Nextcloud's own WebDAV diff --git a/SYNC-PLAN.md b/SYNC-PLAN.md index d749961..35c0e63 100644 --- a/SYNC-PLAN.md +++ b/SYNC-PLAN.md @@ -57,60 +57,13 @@ setup.** vlda-01's copy left stopped as a cold backup, not decommissioned. #### 2b. Ongoing backup: `git.jayfield.org` (alpha) → `vlda-01.home.jayfield.org` -**Status: sketch only, not built.** Now that alpha is the canonical live -Gitea, vlda-01's stopped copy will just get staler over time unless -something keeps feeding it. Goal here is a **backup**, not a hot standby -or live mirror — point-in-time recovery if alpha's Gitea is ever lost, -not instant failover. - -**Same network constraint as everything else in this plan**: alpha has no -route into the home LAN (that's exactly the tunnel dependency removed -2026-07-19), and vlda-01 isn't guaranteed to be on. So the pull has to be -**initiated by vlda-01** whenever it happens to be up — never alpha -reaching in. - -**Why not a raw `rsync` of `/srv/gitea/data`**: the DB is a live SQLite -file (`gitea.db`); rsyncing it while Gitea is writing risks grabbing it -mid-transaction and shipping a corrupt copy. Gitea ships a purpose-built -`gitea dump` command for exactly this (confirmed available on this -install/version) — it produces one consistent archive (DB + repos + LFS + -config together), which is also what Gitea's own restore procedure -expects, rather than reinventing consistency handling. - -**Design**: -1. **On alpha** — a nightly cron job runs - `docker exec -u git gitea gitea dump -f /data/backups/gitea-dump-.zip --skip-index` - (`--skip-index` drops the regenerable Bleve search index, which isn't - needed to restore and just adds size/time) into - `/srv/gitea/data/backups/` (a subdirectory of the same bind mount, so - it's already host-inspectable without extra volumes). Prune to the - last ~14 daily archives in the same job — at the repo sizes seen here - (~4GB total) that's a bounded, comfortable footprint against alpha's - 136GB free. -2. **On vlda-01** — a cron job (running whenever vlda-01 is powered on; - picks up wherever it left off, same "catches up automatically" - philosophy as the Syncthing files plan above) pulls the newest archive - from alpha via `rsync`/`scp` over SSH into a dedicated backup directory - (plenty of headroom on the Unraid array — 3.7TB free at last check). - Keep its own retention window there too, independent of alpha's. -3. **Restore path, if ever needed**: manual, deliberate — stop the target - Gitea container, unpack the chosen dump's DB/repos/etc. into `/data` - per Gitea's documented restore steps, restart, verify. Not automated; - matches how every other risky operation in this repo is handled - (careful, verified, not blind). -4. **Explicitly out of scope for this design**: real-time replication, - automatic failover, or DNS/traffic cutover — none of that fits a - personal git host's actual usage pattern (a handful of commits per - session), and the added complexity/failure surface wouldn't be worth - it. Revisit if that ever changes. -5. **Future consolidation**: if/when Syncthing (item 1 above) actually - gets built, it could carry these dump archives too instead of a - separate rsync/cron pull — one sync mechanism instead of two. Not - worth blocking this on Syncthing existing first, though. - -**Open question before building**: nightly cadence assumed above — confirm -that matches how often this actually needs protecting (probably fine, -given commit frequency, but worth a sanity check before automating it). +**Status: built and verified, 2026-07-26 — see `README.md`'s "Gitea backup +to vlda-01" section for the full live setup.** Point-in-time backup, not a +hot standby/live mirror; explicitly out of scope: real-time replication, +automatic failover, DNS/traffic cutover — none of that fits a personal +git host's actual usage. Future consolidation candidate: if/when +Syncthing (item 1 above) gets built, it could carry these archives too +instead of a separate pull mechanism. ### 3. Jellyfin → scoping question, not yet planned