Configuration reference
Everything lives in one file, restoreguard.json, in the directory you run
from (or pass -c/--config <path>). This page explains each section; the
complete annotated sample at the bottom is a copy-paste
starting point (it also ships in the repo as restoreguard.sample.json).
Three global rules:
-
Every section is optional. A check only runs when its section is present. Start with one section and grow.
-
Hosts are SSH aliases from the operator machine’s
~/.ssh/config(key-based auth — see Getting started). -
Comments and trailing commas are allowed in the file, so you can annotate your own config.
The guided wizard (bare restoreguard on first run, or the setup menu entry)
writes the common sections for you, with live validation. The sections it
doesn’t cover are marked below — add those by hand from the sample.
dockerHosts — mount drift & stale configs
"dockerHosts": [
{ "alias": "mydockerhost", "dockerPath": "docker" },
{ "alias": "myotherhost", "dockerPath": "/usr/bin/docker" }
]
Audits every running container: declared compose bind-mounts/volumes vs. what
is actually mounted, and whether the container still matches its current
compose file (stale config). Use an absolute dockerPath when docker isn’t in
the SSH user’s PATH.
Host needs: Docker daemon access (root or the docker group), compose
plugin ≥ 2.17, read access to the compose project directories and env files.
logicalDbBackups — database dump jobs
"logicalDbBackups": [
{
"host": "mydockerhost",
"path": "/var/backups/db-prod",
"coversHosts": ["mydockerhost"],
"maxDumpAgeHours": 26,
"method": "pg_dumpall",
"requireProdNaming": true
}
]
A list — one entry per dump job you run. Dump files are expected to be named
<container>_<yyyyMMdd>.sql.gz.
|
where the dump files land (checked over SSH) |
|
which `dockerHosts’ database containers this job is responsible for |
|
freshness limit; |
|
what the job runs: |
|
|
Host needs: read access to the dump directory.
pveNodes — Proxmox image backups
"pveNodes": [
{ "alias": "mypve", "node": "pve", "backupStorages": ["pbs-datastore", "local-dumps"] }
],
"pbsMaxSnapshotAgeHours": 26
Checks that every VM/LXC on the node has a fresh image backup.
backupStorages lists PBS datastores and plain vzdump directory storages —
the storage type decides the tier, and either counts as coverage.
| If several nodes share one PBS datastore, list that datastore on exactly one node — otherwise the same backups are counted twice. |
Host needs: root on the PVE node (pvesh).
fileBackups — restic, borg, kopia, snapper, dir, Home Assistant
"fileBackups": [
{ "name": "restic app configs", "kind": "restic", "alias": "mydockerhost",
"repo": "/mnt/nas/restic-repo", "passwordFile": "/root/.restic-pass", "maxAgeHours": 26,
"canaryPath": "/opt/apps/docker-compose.yml" },
{ "name": "borg system configs", "kind": "borg", "alias": "myotherhost",
"repo": "/var/backups/borg", "passwordFile": "/root/.borg-pass", "maxAgeHours": 26,
"canaryPath": "/etc/fstab" },
{ "name": "kopia snapshots", "kind": "kopia", "alias": "mydockerhost", "maxAgeHours": 26 },
{ "name": "btrfs snapshots (snapper)", "kind": "snapper", "alias": "myotherhost",
"snapperConfig": "root", "maxAgeHours": 26 },
{ "name": "volume tarballs", "kind": "dir", "alias": "myotherhost",
"path": "/opt/volume-backups", "maxAgeHours": 26 },
{ "name": "home-assistant full backup", "kind": "haos", "alias": "mypve",
"vmid": 9000, "maxAgeHours": 192 }
]
One entry per backup source; each configured source must produce artifacts — an empty repository or directory is a RED, not a shrug.
canaryPath (restic/borg) opts into the
restore canary:
a small file that must restore from the latest snapshot on every audit — pick one
that is always in the backup, like a compose or config file. Per kind:
| Kind | Extra fields | Host needs |
|---|---|---|
|
|
repo + password file readable on the host — the secret never leaves it |
|
|
repo + passphrase file readable on the host |
|
— |
connect the host to its repo once ( |
|
|
snapper installed, the named config existing (btrfs) |
|
|
a readable directory of archive files (e.g. offen/docker-volume-backup tarballs) |
|
|
Home Assistant OS full backups, read via the Proxmox qemu guest agent: agent enabled in the HA VM, root on its PVE host |
sqliteBackupDirs — SQLite hot-copy detection
"sqliteBackupDirs": [
{ "name": "appdata copies", "alias": "mydockerhost", "path": "/backups/appdata" }
]
One entry per rsync/plain-copy backup folder of app data. The folder is scanned
recursively for -wal/-shm files on every audit — finding one means the
SQLite database next to it was copied while open (the silently-corrupt backup).
Host needs: read access to the folder (GNU find).
offsiteJobs — rclone off-site syncs
"offsiteJobs": [
{ "name": "pbs to onedrive", "alias": "mypve",
"logPath": "/var/log/pbs-onedrive-sync.log",
"rcloneRemote": "onedrive:", "maxSyncAgeHours": 26 },
// log-only (no capacity probe — e.g. a backend without `rclone about`):
{ "name": "nightly usb copy", "alias": "myotherhost",
"logPath": "/var/log/usb-sync.log" }
]
One entry per scheduled sync job. The job’s wrapper script must log
=== <yyyy-MM-dd HH:mm> … sync start === when it starts and
=== sync finished rc=N === when it ends — a minimal wrapper:
echo "=== $(date '+%F %R') $(hostname) sync start ===" >> "$LOG"
rclone sync /path remote:bucket >> "$LOG" 2>&1
echo "=== sync finished rc=$? ===" >> "$LOG"
rcloneRemote is optional: set it to also watch the remote’s occupancy
(rclone about; skip it for backends that don’t support about).
Host needs: read access to the log; with rcloneRemote, the host’s rclone
remote must authenticate.
pbsOffsite is the older single-job flavor of this section and keeps
working; new configs should prefer offsiteJobs.
|
zfsReplications — sanoid/syncoid & zfs send
"zfsReplications": [
{ "name": "tank/data @ pve -> nas",
"sourceAlias": "mypve", "sourceDataset": "tank/data",
"targetAlias": "mynas", "targetDataset": "backup/pve/data",
"maxSnapshotAgeHours": 26, "maxReplicaAgeHours": 26 },
// snapshot-only (no replica): watch that the snapshot job keeps running
{ "name": "tank/scratch snapshots",
"sourceAlias": "mypve", "sourceDataset": "tank/scratch" }
]
One entry per dataset. target* fields come as a pair or not at all: with a
target, the replica’s newest snapshot is checked too (zfs send/syncoid still
shipping); without, only source snapshot freshness (sanoid still running).
Host needs: zfs on PATH for the SSH user; read access is enough — the probe
is zfs list -H -p -t snapshot (read-only, no zfs send/recv is ever run).
trueNas — ZFS snapshots, cloud sync, pools
"trueNas": {
"alias": "mytruenas",
"excludeDatasets": [],
"maxSnapshotAgeHours": 26,
"maxSyncAgeHours": 26
}
Pool health and scrub age, per-dataset capacity, snapshot freshness, cloud-sync
task state, and top-level datasets that never leave the box. excludeDatasets
are dropped before they enter the model at all — use it for datasets that are
intentionally not audited.
Host needs: a TrueNAS SCALE admin user (midclt call, read-only queries).
pbsOffsite — off-site copy of the PBS datastore (hand-edited)
"pbsOffsite": {
"alias": "mypve",
"logPath": "/var/log/pbs-sync.log",
"rcloneRemote": "remote:",
"targetName": "pbs-datastore",
"maxSyncAgeHours": 26
}
For labs that sync the PBS datastore off-site with an rclone wrapper script.
The script’s log must mark runs with === <ts> sync start === /
=== sync finished rc=N === lines; occupancy of the remote comes from
rclone about.
Host needs: read access to the log; the host’s rclone remote must authenticate non-interactively.
pbsMaintenance — PBS GC & verification (hand-edited)
"pbsMaintenance": {
"execAlias": "mypve",
"containerId": 110,
"datastore": "main",
"host": "pve",
"maxGcAgeDays": 7,
"maxVerifyAgeHours": 50,
"maxSyncJobAgeHours": 26,
// bare-metal hosts backed up with proxmox-backup-client (backup ids under
// host/ in the datastore — usually the hostname):
"hostBackups": ["mydockerhost"],
"maxHostBackupAgeHours": 26
}
The hygiene the PBS UI won’t nag you about: garbage collection has run
recently, verify jobs exist, and the last completed verification passed and
is fresh. A failed verification means corrupted chunks — that is a RED. Read by
running proxmox-backup-manager inside the PBS container from its PVE host
(pct exec), so no PBS API token is needed.
Two more things ride on the same probe:
-
PBS→PBS sync jobs are auto-discovered (
sync-job list) — when any exist, the last completed sync must have succeeded and be fresh. Nothing to configure beyondmaxSyncJobAgeHours. -
hostBackups(optional): bare-metal hosts backed up straight to PBS withproxmox-backup-client. Each listed backup id must have a fresh snapshot underhost/in the datastore (found via the datastore’s on-disk path — still read-only).
Host needs: root on the PVE host that runs the PBS container.
smartHosts — physical disk health
"smartHosts": ["mypve"]
SMART overall-health per disk. List the machines that own the physical disks — hypervisors and bare-metal boxes. A virtualized NAS only sees virtual disks, so probe its host instead.
Host needs: root (raw device access), smartmontools installed.
reportingFile — where audit reports are delivered (a shared, standalone file)
Report destinations live in their own file (the wizard writes reporting.json),
and the main config just points at it:
"reportingFile": "reporting.json"
The point of the separate file is sharing: the same reporting.json is a
self-contained connection layout, so a second tool — for example HCC — can read
the exact same file, connect to the same folder/bucket/database, and pull back
the reports RestoreGuard wrote there. RestoreGuard writes; another tool reads;
one file describes the connection for both. The path is relative to the main
config. (An inline reporting section is still accepted as a legacy form, but
the wizard writes the separate file — use reportingFile for anything new; set
one or the other, not both.)
{
"folder": { "id": "local-spool", "enabled": true, "path": "/var/lib/restoreguard/reports", "keepLast": 30 },
"s3": {
"id": "offsite-minio", "enabled": true,
"endpoint": "http://192.168.1.10:9000", "bucket": "backups",
"prefix": "restoreguard/", "region": "us-east-1", "forcePathStyle": true,
"accessKeyFile": "/etc/restoreguard/s3.access", "secretKeyFile": "/etc/restoreguard/s3.secret"
},
"mongo": {
"id": "mongo-main", "enabled": true,
"connectionStringFile": "/etc/restoreguard/mongo.uri",
"database": "restoreguard", "collection": "reports"
}
}
Each destination has an id (any string, must be unique — the wizard
auto-generates a GUID, edit it to taste) and enabled (default true).
RestoreGuard writes to every enabled destination. Each report copy is stamped
with its own target (the id it was saved to) plus a content hash that is
identical across copies — so a reader (HCC) tells where and when a report was
saved and dedups by hash (see Automation). A duplicate
id is rejected when the file is read.
Every audit delivers its JSON report (the same document --json prints) to
all destinations in the file, in parallel. Each is optional; with no
reportingFile (and no inline reporting), reports go to the per-user default
folder (Documents\RestoreGuard\reports on Windows,
~/.local/share/restoreguard/reports elsewhere — overridable with the
RESTOREGUARD_REPORTS_DIR environment variable). A destination that can’t be
written fails the run with exit code 1 — a report that silently went nowhere
would blind every consumer. The guided setup lives behind the r menu entry,
which live-probes each destination (a write + delete round-trip) as you
answer; doctor re-verifies them on every preflight.
-
folder— writesrg-report-<utc-timestamp>.jsonper audit plus an atomically-replacedlatest.json, the stable filename scripts should read. Files appear via temp-file + rename, so a watcher never sees a half-written report.pathis optional (Enter = the default folder) and resolves relative to the reporting file;keepLastprunes all but the newest N timestamped reports (omit to keep everything). -
s3— any S3-compatible store (MinIO, Garage, R2, AWS). Puts the timestamped key and<prefix>latest.jsoninto an existing bucket.forcePathStyle: true(the default) is what self-hosted stores expect; set itfalsefor AWS’s bucket-in-hostname addressing. Credentials go inline (accessKey/secretKey) or — better — in root-only files via the\*Filevariants (relative paths resolve against the reporting file’s directory, so the file stays portable to another tool). -
mongo— inserts each report as one document, so history is queryable (db.reports.find({overall:"red"}), bygeneratedAt, byfindings.ruleId, …).connectionStringorconnectionStringFile;database/collectiondefault torestoreguard/reports. MongoDB-wire-compatible servers (e.g. FerretDB) work here unchanged.
The full annotated destinations file ships as reporting.sample.json.
Not covered by the first-run wizard — configure with the r menu entry or
by hand.
suppressionsFile
"suppressionsFile": "suppressions.json"
Path to the suppressions list, relative to the config file.
The full annotated sample
Comments and trailing commas are allowed, so this works as-is — save it as
restoreguard.json, delete the sections you don’t have, and replace the
placeholder aliases:
{
// RestoreGuard config — EVERY section is optional; a check only runs when its
// section is present. Hosts are SSH ALIASES from the operator machine's
// ~/.ssh/config (key-based, passwordless auth — the audit runs ssh -o BatchMode=yes).
// Full guide: https://ofbirds.org/docs/goose/ (in-repo: docs/modules/ROOT/pages/).
// Comments and trailing commas are allowed in this file.
// Docker hosts to audit for mount drift (declared compose config vs live mounts).
"dockerHosts": [
{ "alias": "mydockerhost", "dockerPath": "docker" },
// Use an absolute path when docker isn't in the SSH user's PATH:
{ "alias": "myotherhost", "dockerPath": "/usr/bin/docker" }
],
// Nightly logical DB dump jobs (files named <container>_<yyyyMMdd>.sql.gz).
// coversHosts: which dockerHosts' DB containers each job is responsible for.
// method: what the job runs (pg_dumpall | mysqldump | mongodump) — a container
// whose ENGINE needs a different method is a RED method-mismatch (a pg_dumpall
// of a MariaDB or DocumentDB "succeeds" but is unrestorable).
// requireProdNaming: true mirrors jobs that dump only containers named 'prod'
// (skipping 'staging'); false expects a dump for EVERY running DB container.
"logicalDbBackups": [
{
"host": "mydockerhost",
"path": "/var/backups/db-prod",
"coversHosts": ["mydockerhost"],
"maxDumpAgeHours": 26,
"method": "pg_dumpall",
"requireProdNaming": true
},
{
"host": "myotherhost",
"path": "/var/backups/db-mysql",
"coversHosts": ["myotherhost"],
"maxDumpAgeHours": 26,
"method": "mysqldump",
"requireProdNaming": false
}
],
// Proxmox VE nodes. backupStorages lists PBS datastores AND plain vzdump dir
// storages — the storage type decides the tier, and either counts as image
// coverage for a guest. List a shared PBS datastore on EXACTLY ONE node.
"pveNodes": [
{ "alias": "mypve", "node": "pve", "backupStorages": ["pbs-datastore", "local-dumps"] }
],
"pbsMaxSnapshotAgeHours": 26,
// ZFS snapshots & replication outside TrueNAS (sanoid/syncoid, zfs send).
// Per dataset: the source must keep getting snapshots; with target* set (a pair),
// the replica's newest snapshot must be fresh too — a dead replication LOOKS fine
// because the replica keeps its old snapshots. Probe is `zfs list` (read-only).
"zfsReplications": [
{
"name": "tank/data @ mypve -> mynas",
"sourceAlias": "mypve",
"sourceDataset": "tank/data",
"targetAlias": "mynas",
"targetDataset": "backup/pve/data",
"maxSnapshotAgeHours": 26,
"maxReplicaAgeHours": 26
},
{
// snapshot-only: no replica, just "is sanoid/the snapshot job alive".
"name": "tank/scratch snapshots",
"sourceAlias": "mypve",
"sourceDataset": "tank/scratch"
}
],
// TrueNAS SCALE box: pool health/scrub, per-dataset capacity, snapshot freshness,
// cloud-sync task state. excludeDatasets are dropped BEFORE entering the model.
"trueNas": {
"alias": "mytruenas",
"excludeDatasets": [],
"maxSnapshotAgeHours": 26,
"maxSyncAgeHours": 26
},
// SQLite hot-copy detection: rsync/plain-copy backup folders of app data
// (vaultwarden, *arr, Home Assistant...). Scanned RECURSIVELY for -wal/-shm
// files — those exist next to a database only while it is OPEN, so inside a
// backup they prove the .db was copied mid-write (restores may be corrupt).
"sqliteBackupDirs": [
{
"name": "appdata copies",
"alias": "mydockerhost",
"path": "/backups/appdata"
}
],
// Off-site sync jobs: any scheduled rclone script that logs
// "=== <yyyy-MM-dd HH:mm> ... sync start ===" / "=== sync finished rc=N ===".
// rcloneRemote is optional (capacity via `rclone about`; skip for backends
// without about-support). Every configured job must have run: an empty log
// is RED offsite/never-ran.
"offsiteJobs": [
{
"name": "pbs to onedrive",
"alias": "mypve",
"logPath": "/var/log/pbs-onedrive-sync.log",
"rcloneRemote": "onedrive:",
"maxSyncAgeHours": 26
},
{
// log-only: no capacity probe.
"name": "nightly usb copy",
"alias": "myotherhost",
"logPath": "/var/log/usb-sync.log"
}
],
// LEGACY: the older single-job, PBS-flavored version of offsiteJobs — still
// works, but prefer offsiteJobs for anything new.
"pbsOffsite": {
"alias": "mypve",
"logPath": "/var/log/pbs-onedrive-sync.log",
"rcloneRemote": "onedrive:",
"targetName": "pbs-nvme",
"maxSyncAgeHours": 26
},
// PBS GC + verify-job hygiene, read by running proxmox-backup-manager inside the
// PBS container from its PVE host (pct exec — needs root on the PVE host).
"pbsMaintenance": {
"execAlias": "mypve",
"containerId": 110,
"datastore": "main",
"host": "pve",
"maxGcAgeDays": 7,
// Last completed verification must be OK and younger than this (daily
// schedule + margin). A failed verification is RED - corrupted chunks.
"maxVerifyAgeHours": 50,
// PBS->PBS sync jobs are AUTO-DISCOVERED: when any exist, the last completed
// sync must have succeeded and be younger than this.
"maxSyncJobAgeHours": 26,
// Bare-metal hosts backed up with proxmox-backup-client: each backup id
// (usually the hostname, under host/ in the datastore) must have a fresh
// snapshot. Optional - omit if no host backs up straight to PBS.
"hostBackups": ["mydockerhost"],
"maxHostBackupAgeHours": 26
},
// SMART health of PHYSICAL disks — probe the machines that own the disks
// (hypervisors, bare-metal NAS). A virtualized NAS only sees virtual disks.
"smartHosts": ["mypve"],
// File-level backup sources. Kinds: "restic" / "borg" (repo + password/passphrase
// file on the host — the secret never leaves it), "dir" (a directory of archive
// files, e.g. offen/docker-volume-backup tarballs), "haos" (Home Assistant OS full
// backups via the Proxmox qemu guest agent). Each configured source MUST produce
// artifacts — an empty repo / dir / no full HA backup is a RED finding.
// canaryPath (restic/borg, optional): the restore canary — a small file that must
// restore from the LATEST snapshot on every audit (streamed + counted on the host;
// nothing is written, no content leaves the machine). Pick a file that is always
// in the backup, like a compose/config file. 0 bytes back = RED.
"fileBackups": [
{
"name": "restic app configs",
"kind": "restic",
"alias": "mydockerhost",
"repo": "/mnt/nas/restic-repo",
"passwordFile": "/root/.restic-pass",
"maxAgeHours": 26,
"canaryPath": "/opt/apps/docker-compose.yml"
},
{
"name": "borg system configs",
"kind": "borg",
"alias": "myotherhost",
"repo": "/var/backups/borg",
"passwordFile": "/root/.borg-pass",
"maxAgeHours": 26,
"canaryPath": "/etc/fstab"
},
{
"name": "myvolume tarballs",
"kind": "dir",
"alias": "myotherhost",
"path": "/opt/volume-backups",
"maxAgeHours": 26
},
{
"name": "home-assistant full backup",
"kind": "haos",
"alias": "mypve",
"vmid": 9000,
"maxAgeHours": 192
},
{
"name": "btrfs snapshots (snapper)",
"kind": "snapper",
"alias": "myotherhost",
"snapperConfig": "root",
"maxAgeHours": 26
},
{
// kopia keeps its own repo connection+password on the host: run
// `kopia repository connect ...` there once; listing then needs no secret.
"name": "kopia snapshots",
"kind": "kopia",
"alias": "mydockerhost",
"maxAgeHours": 26
}
],
// Where each audit DELIVERS its JSON report (the same document --json prints).
// The destinations live in their OWN self-contained file so a SECOND tool — e.g.
// HCC — can read the SAME file, connect to the same folder/bucket/DB, and pull the
// reports RestoreGuard wrote there. This is just a pointer to that file (path
// relative to this config); its schema is in reporting.sample.json. Omit it and
// reports go to the per-user default folder (Documents\RestoreGuard\reports on
// Windows, ~/.local/share/restoreguard/reports elsewhere; RESTOREGUARD_REPORTS_DIR
// overrides). The wizard (`r` menu) writes reporting.json and this pointer for you.
"reportingFile": "reporting.json",
// Known exceptions with reasons + dates — a JSON list of entries like:
// { "host": "mydockerhost", "service": "myapp-staging-postgres-1",
// "ruleId": "db-backup/uncovered", "reason": "staging data is disposable",
// "decidedOn": "2026-06-29", "expires": "2026-12-31", // both optional:
// "retriggerCondition": "revisit if staging holds real data" }
// Rule ids: the user guide's "What gets checked" page. Suppressed findings stay
// visible in every report; expired/dead entries become findings themselves.
"suppressionsFile": "suppressions.json"
}