Pull-based backup vault for cockpit-nspawn hosts. The vault initiates every backup over SSH with a forced, read-only command on the source host - a compromised nspawn host never holds credentials that could reach or modify the vault. Backups land on ZFS, snapshotted after every pull, with GFS retention and a dead-man's switch that alerts via Pushover/Slack if a source host stops reporting successful pulls.
Tested and verified end-to-end on AlmaLinux 10.2 (2026-07-01): ZFS via OpenZFS DKMS, RPM install, SSH key setup, and a full pull cycle against a production nspawn host.
Source host (nspawn-cockpit, /usr/local/lib/nspawn-pull/):
dispatch.sh, snapshot-db.sh, restore-after-backup.sh - a forced SSH command
that only ever runs a whitelisted set of operations for the vault's key.
Vault (this project, /usr/libexec/nspawn-vault/):
pull-host.sh / pull.sh initiate the pull over SSH (read-only rsync via
rrsync -ro on the source side), check-stale.sh runs the dead-man's switch,
gfs-prune.sh / prune-all.sh apply retention. Results land as ZFS snapshots
under vault/<host>/<container>.
The vault pulls - it initiates every connection. The source host never holds a key that could reach or modify the vault; the vault's key on the source host can only ever run the forced dispatcher command, nothing else.
- AlmaLinux 10 (or another EL10 derivative) for the vault host
- A dedicated block device for the ZFS pool (e.g.
/dev/vdb) - One or more nspawn-cockpit hosts already reachable over SSH, with
/usr/local/lib/nspawn-pull/dispatch.shinstalled (currently manual - a toggle in cockpit-nspawn itself is planned but not built yet)
This package Requires: zfs, which is not in AlmaLinux's own repos (CDDL/GPL license
conflict). Installing ZFS has to happen before this RPM, or dnf can't resolve the
dependency - so the setup script ships in its own package, nspawn-vault-zfs-bootstrap
(../zfs-bootstrap/), which has no dependencies of its own and can therefore always be
installed first, on a completely fresh box:
dnf install nspawn-vault-zfs-bootstrap-<version>.<dist>.noarch.rpm
nspawn-vault-setup-zfsThis adds the OpenZFS repo (zfs-release-3-0.el10.noarch.rpm - the version number in
that filename has changed before, check https://zfsonlinux.org/epel/ if it 404s) and
installs zfs-dkms, built against the running kernel.
/usr/libexec/nspawn-vault/init-pool.sh /dev/vdbDestructive - wipes the target device. Requires typing ja to confirm. Pool name
defaults to vault; pass a second argument to override.
dnf install nspawn-vault-<version>.<dist>.noarch.rpmInstalls to /usr/libexec/nspawn-vault/ (scripts), /etc/nspawn-vault/ (config),
/usr/lib/systemd/system/ (timers), /var/lib/nspawn-vault/state/ (pull status).
Timers are installed but disabled - AlmaLinux's default preset policy
(disable *) means nothing auto-starts; enable explicitly in step 6.
ssh-keygen -t ed25519 -f /root/.ssh/nspawn-vault -N ""One key per vault, never copied to source hosts.
On every nspawn-cockpit host this vault should pull from, append the public key with a forced, restricted command:
echo 'restrict,command="/usr/local/lib/nspawn-pull/dispatch.sh" '"$(cat /root/.ssh/nspawn-vault.pub)" \
>> /root/.ssh/authorized_keysrestrict + command= means this key can only ever run dispatch.sh, regardless of
what the vault asks for - dispatch.sh itself whitelists the exact commands allowed
(snapshot-db <name>, restore-after-backup <name>, and the rsync read-only
transfer). Multiple vaults can each have their own line without disturbing others.
# List containers to pull from this host, one per line
mkdir -p /etc/nspawn-vault/<source-host>/
echo "<container-name>" >> /etc/nspawn-vault/<source-host>/containers
# Notification credentials for the dead-man's switch (optional but recommended)
cp /etc/nspawn-vault/notify.conf.example /etc/nspawn-vault/notify.conf
chmod 600 /etc/nspawn-vault/notify.conf
# fill in PUSHOVER_TOKEN / PUSHOVER_USER or SLACK_URL
# Enable the always-on timers
systemctl enable --now nspawn-vault-check.timer nspawn-vault-prune.timer
# Enable one templated pull timer per source host
systemctl enable --now "nspawn-vault-pull@<source-host>.timer"systemctl start "nspawn-vault-pull@<source-host>.service"
journalctl -u "nspawn-vault-pull@<source-host>.service" -f
zfs list -t snapshot -r vault
cat /var/lib/nspawn-vault/state/*.jsonA successful pull writes {"result":"success","ts":"...","snap":"vault/<host>/<name>@<timestamp>"}
per container to /var/lib/nspawn-vault/state/.
nspawn-vault-prune.timer runs prune-all.sh daily at 04:00, applying
Grandfather-Father-Son retention across every dataset. Defaults (24 hourly / 7 daily /
4 weekly / 12 monthly / 3 yearly) live in /etc/nspawn-vault/gfs.conf - copy
gfs.conf.example to override.
Source-side config at /etc/cockpit-nspawn/pull/<container>.conf controls how the
source host handles databases before each pull - DB_TYPE=mariadb|postgres for a
live dump, or STOP_DURING_BACKUP=true to stop the container for the duration of the
pull. See cockpit-nspawn's own docs for the source-side half of this.
# Source lives at /root/nspawn-vault/engine/, but %setup in the spec expects
# the tarball's top-level directory to be named "nspawn-vault" - --transform
# renames it during archiving without needing to physically relocate anything.
tar czf nspawn-vault.tar.gz \
--transform 's,^engine,nspawn-vault,' \
-C /root/nspawn-vault engine
rpmbuild -bb nspawn-vault.specA standalone web dashboard (not a Cockpit module) lives alongside this project
at ../web/ - see ../web/README.md. It shows pull status per host/container,
the same dead-man's-switch staleness this project's check-stale.sh computes,
and lets admins edit GFS retention / notification settings / source hosts
without touching config files by hand. Built and verified 2026-07-02.
- ZFS repo/package names for EL10 were only confirmed working 2026-07-01 - re-verify
if
setup-zfs.shstarts failing on a future AlmaLinux/OpenZFS release - Multi-vault replication (
zfs send/receiveviasyncoid) is a future consideration, not implemented - RPM builds also run through an internal CI pipeline now (see
CLAUDE.md), but the manualrpmbuildsteps above remain the standalone source of truth