Greylag Goose in the ofbirds flock — the watch-bird: the sacred geese of Juno whose alarm saved Rome from the night attack on the Capitol. First app in the homelab tooling category (RestoreGuard stays the technical name on GitHub/GHCR; Greylag Goose is the product identity).
📘 User guide: https://ofbirds.org/docs/goose/ · Product page: https://ofbirds.org/goose
The most viscerally-described homelab failure: backups that report success daily while silently dumping empty directories — a renamed bind-mount during a compose refactor, a pg_dump pointing at an old hostname after a VM→LXC migration, an offsite target that's been full and silently rejecting snapshots for 60 days. People only discover it during a real disaster.
One read-only CLI that cross-checks what your backups claim against what your infrastructure actually is, and prints a RED/YELLOW/GREEN report per service:
- Mount drift — docker compose declared bind-mounts/volumes vs. what the container actually mounts (the classic "backing up an empty folder" cause).
- Stale config — a container still running an old compose config (config-hash drift), so the next restart silently changes behavior.
- Logical DB dump coverage — every live prod database container has a fresh,
non-empty dump; method mismatch flagged (e.g.
pg_dumpallagainst a DocumentDB image,mysqldumpvspg_dumpper engine); naming-convention contracts enforced. - Image backups (Proxmox) — every VM/LXC is covered by PBS or vzdump, backups
are fresh, orphan backups flagged; PBS datastore GC ran recently, verify jobs
exist and the last verification completed, passed, and is fresh; PBS→PBS sync
jobs succeed and are fresh; bare-metal
proxmox-backup-clienthosts have fresh host backups. - File-level backup tools — restic, borg, kopia, snapper (btrfs), Home Assistant native backups, or plain archive directories: snapshots exist, are fresh, and aren't suspiciously small.
- SQLite hot-copy detection — rsync/plain-copy backups of app data
(vaultwarden, *arrs, Home Assistant) scanned for
-wal/-shmfiles: proof a database was copied while open — the silently-corrupt SQLite backup. - Restore canary — snapshots existing ≠ backups restoring. Opt a restic/borg source into a per-audit restore drill: a sentinel file is streamed out of the latest snapshot and byte-counted on the host (nothing written, no content over the wire). 0 bytes back — wrong passphrase, corrupt chunks, path fell out of the backup — is RED.
- 3-2-1 hygiene (Proxmox) — a guest whose every image backup sits on non-shared storage of its own node gets flagged: one disk or host failure takes the guest and all its copies together.
- ZFS snapshots & replication — sanoid/syncoid or plain
zfs sendon any host: the snapshot job still produces, and the replica's newest snapshot is fresh (a dead replication looks fine — the replica keeps its old snapshots). - TrueNAS — ZFS snapshot freshness, cloud-sync tasks succeeding, top-level datasets that never leave the box, pool health + scrub age.
- Off-site freshness & capacity — every scheduled rclone sync job actually ran, succeeded, and is recent (a job that never ran is RED, not invisible); the destination isn't silently full.
- Disk health — SMART status on the hypervisors.
- Suppression hygiene — accepted-risk entries are first-class and fail loud: expired or dead suppressions become findings themselves, never silent.
Everything is read-only: no state is written to any audited host. There is no AI in the audit path — the engine is deterministic plumbing, and any AI-assisted presentation will only ever consume the finished report, optionally and bring-your-own-key.
(The full user guide is the Antora component in docs/ — published on the
ofbirds docs site under goose. The README keeps this condensed version.)
RestoreGuard is a single CLI that audits your infrastructure read-only over SSH. It stores no credentials of its own — it rides on the SSH setup you already have. You need:
- OpenSSH client on PATH (
ssh). (No .NET needed for the released binaries; see install options below.) - SSH aliases + keys. Every
aliasin the config must resolve via~/.ssh/configwith key-based, passwordless auth. The audit runsssh -o BatchMode=yes, which cannot answer prompts, so:- the key must not have an interactive passphrase prompt (use an agent), and
- connect to each host once manually first so its host key lands in
known_hosts— a first-contact prompt fails the audit.
| Config section | Runs on the host | SSH user needs |
|---|---|---|
dockerHosts |
docker inspect, docker compose config |
Docker daemon access (root or docker group); compose plugin ≥ 2.17; read access to compose project dirs + env files |
logicalDbBackup |
find over the dump dir |
read access to the dump directory |
pveNodes |
pvesh get (guests, storage, backup content — PBS and vzdump dir storages) |
root on the PVE node (pvesh) |
fileBackups |
per kind: restic snapshots, borg list, kopia snapshot list, snapper list, find over an archive dir, or qm guest exec … ha backups; with canaryPath also a dump/extract --stdout … | wc -c restore drill |
restic/borg: repo + password/passphrase file readable on the host; dir: readable path; haos: qemu guest agent enabled in the HA VM, root on its PVE host |
zfsReplications |
zfs list -H -p -t snapshot on source (and replica) |
zfs on PATH; read access (no zfs send/recv is ever run) |
offsiteJobs |
tail the job's log; optional rclone about |
read the log; with rcloneRemote, the host's remote must authenticate |
sqliteBackupDirs |
recursive find for *-wal/*-shm |
read access to the backup folder |
pbsOffsite (legacy — prefer offsiteJobs) |
tail the sync log, rclone about |
read the log; the host's rclone remote must authenticate |
pbsMaintenance |
pct exec <CT> -- proxmox-backup-manager (GC, verify + sync jobs, datastore list); ls of host/<id> snapshot dirs for hostBackups |
root on the PVE host that runs the PBS container |
smartHosts |
smartctl -H |
root (raw device access); smartmontools installed |
Targets are assumed Linux-ish with standard tools (GNU find, awk, tail).
No .NET required — binaries are fully self-contained. Pick your poison
(all built by the tag-triggered release pipeline in .github/workflows/release.yml):
# One-liner (Linux/macOS): detects OS/arch, installs to ~/.local/bin
curl -fsSL https://raw.githubusercontent.com/OfBirds/RestoreGuard/main/scripts/install.sh | sh
# Debian/Ubuntu package
sudo dpkg -i restoreguard_<version>_amd64.deb # from the releases page
# Docker (image on ghcr; mount SSH config read-only + a work dir with your
# restoreguard.json — /work is the cwd, so the config is found like on a host;
# use `-c /work/other.json` for a custom name)
docker run --rm -v ~/.ssh:/root/.ssh:ro -v $PWD:/work \
ghcr.io/ofbirds/restoreguard doctor
# Or just grab the tar.gz/zip for your platform from the releases pageFor .NET users, the global-tool route also works
(dotnet tool install -g RestoreGuard once on NuGet.org; from a local build:
dotnet pack src/RestoreGuard.Cli -c Release -o nupkg && dotnet tool install -g --add-source ./nupkg RestoreGuard).
Linux note (dotnet-tool route only): if your
dotnetcame from a manual install rather than a package manager, exportDOTNET_ROOT=$(dirname $(readlink -f $(which dotnet)))and add~/.dotnet/toolsto PATH. The standalone binaries/deb/Docker need none of this.
Then, from any directory:
restoreguard # first run: guided setup wizard writes restoreguard.json;
# after that: a small interactive menu (audit/doctor/json)
restoreguard doctor # preflight: verifies every configured requirement per host
restoreguard audit # the audit: colored RED/YELLOW/GREEN report
restoreguard audit --json # stable machine-readable report on stdout
restoreguard help-c/--config <path> selects a config file (default ./restoreguard.json);
restoreguard.sample.json is the annotated template for the advanced sections the
wizard doesn't cover.
Alternatively, build a standalone binary yourself with the .NET 10 SDK (swap the
RID for win-x64 / osx-arm64 as needed):
dotnet publish src/RestoreGuard.Cli -c Release -r linux-x64 --self-contained \
-p:PublishSingleFile=true -o dist
dist/RestoreGuard.Cli audit--jsonis the integration surface (findings, suppressed findings, active suppressions, provider errors, counts). It carries aschemaVersion(currently1) and ships a canonical JSON Schema per version atcontracts/restoreguard-report.v{N}.schema.jsonfor consumers to validate against; the shape is a contract — additive changes only, and a breaking change bumps the version. There is deliberately no daemon or HTTP endpoint — one-shot CLI + JSON- exit codes compose with the scheduler you already run (cron, systemd timers, CI).
- Report destinations (a standalone
reporting.json, guided by thermenu entry): every audit also delivers the JSON report — to a folder (timestamped history + an atomically-replacedlatest.json), an S3-compatible bucket (MinIO/Garage/R2/AWS), and/or a MongoDB collection, any combination in parallel. The destinations live in their own file (the main config just points at it viareportingFile) so a second tool can read the same file and pull the reports back. With nothing configured it lands in a per-user default folder. - Exit codes:
0all green/yellow,1at least one RED finding or a provider error (partial discovery) or an undeliverable report,2config/preflight problem. Cron-friendly. - Suppressions (
suppressions.json): a list of{host, service, ruleId, reason, decidedOn, expires?, retriggerCondition?}. Suppressed findings are reported in their own section, never silently dropped, and expire on their review date.
RestoreGuard is developed against a real homelab — every provider and check was grounded in live probes of actual infrastructure before it shipped, and the golden-file test fixtures are (sanitized) captures from that lab.
AGPL-3.0-or-later — see LICENSE. The core auditor is and stays free software.