Backup & restore
Snapshot the config tree, restore it atomically, keep the daemon running.
What this is
warden config backup writes a single tar.gz archive of your Warden
configuration tree — the master config.toml plus every *.d/ sibling
directory that Warden is currently using. warden config restore <archive>
takes one of those archives and replaces the live tree atomically, then
signals the running daemon to reload — no systemctl restart needed.
These are operator checkpoints, not differential snapshots. Take one before a risky change (“I’m about to swap blocklists”), keep a few rotating copies on a separate disk, and restore the most recent valid one when something breaks. Warden can also take these checkpoints on a schedule — see automatic backups below.
When you reach for it
- Before a structural change. Editing
[server], swapping the upstream resolvers, importing a new blocklist set — anything where a typo or validation error could leave you without DNS for the LAN. Take a backup first; on failure, restore in seconds. - Migrating to a new host. Run
warden config backupon the old box, copy the archive, runwarden config restoreon the new one. The daemon on the new host picks up the config without a restart. - Recovering from a bad edit. A
warden config editslipped through with a logic error you only noticed an hour later? Restore the latest pre-change archive and let the daemon hot-reload. - Routine off-host backup. A nightly cron that runs
warden config backup --out /mnt/backup/warden/and prunes old archives gives you a rolling window without touching the daemon — or let the built-in scheduler handle it.
You don’t need backup/restore for blocklist data, statistics, audit
logs, or query logs — those are not part of the config tree (see below).
For full system state, snapshot /var/lib/purge-warden/ separately.
What’s included
A backup is a tar.gz of the config tree, anchored at the parent
directory of the master config.toml. It contains:
- The master TOML (
config.toml, or whatever path Warden is using). - Every
*.d/sibling directory that exists at the time of the backup:devices.d/,profiles.d/,groups.d/,blocklists.d/,subnets.d/,schedules.d/,rules.d/.
It does not contain:
- Downloaded blocklist payloads under
/var/lib/purge-warden/lists/. - Statistics snapshots under
/var/lib/purge-warden/data/. - The audit log or the query log.
- IPC tokens, REST API tokens, or any
*.secretfiles. - The PID file.
There is no manifest, no embedded schema version, and no encryption — the
archive is plain tar -czf. Source: src/cli/commands/config/backup.rs.
CLI
| Command | What it does |
|---|---|
warden config backup | Write <backup-dir>/config-<UTC-timestamp>.tar.gz (default dir <config-parent>/backups). |
warden config backup --out <dir> | Write the archive to <dir>/config-<UTC-timestamp>.tar.gz instead. The directory must exist. |
warden config restore <archive> | Validate the archive, replace the live tree, signal the daemon to reload. |
warden config restore --list | List the restore points in the backup dir (date, age, size, newest first). Restores nothing. |
warden config restore --latest | Restore the newest archive in the backup dir without naming it. |
The archive filename is always config-YYYYMMDDTHHMMSSZ.tar.gz; only the
directory is configurable ([backup] dir or --out). Source:
src/cli/mod.rs, src/cli/commands/config/backup.rs.
There is no --json output and no backup info. To see what you can
restore, use warden config restore --list. To inspect an archive’s
contents without restoring, extract it to a scratch directory and run
warden config lint <extracted>/config.toml.
The scheduler-only flags --auto and --reset-auto-failure are covered
under automatic backups.
Where archives go
By default archives land in <config-parent>/backups/ — the backups/
directory next to your master config.toml. Point them elsewhere with the
optional [backup] section:
[backup]
dir = "/srv/purge-backups" # default: <config-parent>/backupsThe daemon ignores [backup] entirely — it is tooling-only. The CLI
(warden config backup, restore --list) and the dashboard restore picker
all read this one setting. (Editing it from the dashboard isn’t in this
release — set it in the file.)
Backup directory permissions. Backups run as the daemon user
(purge-warden), gated by peer-uid, whether triggered from the CLI or the
dashboard. The backup directory must be writable by that user — otherwise
tar fails with Permission denied. The installer (scripts/install.sh)
prepares the default <config-parent>/backups with the correct ownership,
so on a standard install nothing manual is needed. If you point dir at a
custom location (e.g. /srv/purge-backups, a dedicated volume), you own
that path’s permissions:
sudo install -d -m 0755 -o purge-warden -g purge-warden /srv/purge-backups
Examples
A one-shot manual checkpoint before editing:
sudo warden config backup
# → /etc/purge-warden/backups/config-20260505T091322Z.tar.gz
sudo warden config edit
A rolling nightly backup to an off-host mount:
# Nightly Warden config snapshot, rotate after 30 days.
15 3 * * * root /usr/bin/warden config backup --out /mnt/backup/warden/ && \
find /mnt/backup/warden/ -name 'config-*.tar.gz' -mtime +30 -deleteA migration to a new host:
# on the old host
sudo warden config backup --out /tmp/
scp /tmp/config-*.tar.gz newhost:/tmp/
# on the new host (after `warden init`)
sudo warden config restore /tmp/config-20260505T091322Z.tar.gz
A recovery from a bad edit — list the restore points, then take the newest:
sudo warden config restore --list
# config-20260505T091322Z.tar.gz 2 minutes ago 14.2 KiB
# config-20260504T031500Z.tar.gz 1 day ago 14.1 KiB
# config-20260503T031500Z.tar.gz 2 days ago 13.9 KiB
sudo warden config restore --latest
From the dashboard
The TUI Settings tab drives the same engine:
b— back up now. The archive path appears in the footer.R— open the restore picker: every restore point with its date, relative age, and size, newest first. Pick one, pressEnter, confirm withy; Warden restores it and reloads the daemon. The previous config is kept alongside as a.pre-restorecopy, so the restore is recoverable.
Automatic backups
Everything above is manual. Warden can also back up on a schedule: a systemd
timer wakes the binary every hour, and warden config backup --auto decides
whether a backup is actually due. The daemon itself is untouched — the
scheduler is pure tooling, with no new IPC surface.
Configuration
Extend [backup] with up to five optional fields:
[backup]
dir = "/var/lib/purge-warden/backups" # already existed
auto_interval = "24h" # off when absent. "<N>h" or "<N>d", range 1h..720h (30d).
on_change = false # reserved for a future release; parsed but inert.
retention_count = 30 # keep at most N archives (0 = unbounded).
retention_days = 90 # drop archives older than D days (0 = unbounded).
disable_after_failures = 3 # auto-disable after N consecutive failures (0 = never).All optional; existing configs keep loading unchanged. With no
auto_interval the scheduler does nothing. retention_count and
retention_days are OR’d — an archive is dropped if it exceeds either
threshold. Retention only ever removes config-*.tar.gz files; the .lock,
.auto_state, and .pre-restore-* files are never pruned.
How the schedule runs
Two systemd units, both installed for you:
purge-warden-backup.timer— fires hourly (OnUnitActiveSec=1h,OnBootSec=15min,Persistent=true), and catches up at boot if the machine was off when a tick was due.purge-warden-backup.service—Type=oneshot, runswarden config backup --autoas the daemon user.
The timer does not know your interval. Each --auto run reads the state
file and decides:
disabled = true→ exit 0.- No
auto_intervalset → exit 0. now - last_attempt < auto_interval→ exit 0 (“not due”).- Otherwise → acquire the lock, back up, update the state, apply retention.
Changing auto_interval takes effect on the next hourly tick — no
systemctl daemon-reload, no reinstall.
State file
<backup-dir>/.auto_state — JSON, written atomically, safe to read:
sudo cat /var/lib/purge-warden/backups/.auto_state
# {
# "consecutive_failures": 0,
# "last_attempt": "2026-05-29T03:00:00Z",
# "last_outcome": { "kind": "ok" },
# "disabled": false
# }
consecutive_failures— how many--autoruns in a row have failed. Reset to zero on the first success. Manual runs never touch it.last_attempt— RFC3339 UTC of the most recent run (manual updates it too, so other surfaces stay coherent).last_outcome—{"kind":"ok"}or{"kind":"err","message":"…"}.disabled—trueafterdisable_after_failuresconsecutive auto failures. Manual backups ignore it and run anyway.
Re-enable after auto-disable
After disable_after_failures consecutive failures the scheduler sets
disabled = true and later ticks become no-ops. Clear it:
sudo -u purge-warden warden config backup --reset-auto-failure
It resets consecutive_failures and disabled, writes no archive, and
preserves the history (last_attempt / last_outcome). It is idempotent —
a no-op when there is nothing to reset. The next --auto tick retries
normally, and the red banner on the Settings tab clears.
Lock file
<backup-dir>/.lock is created at the start of every backup (manual or
auto) and removed at the end:
- Another backup already running → exit
75(EX_TEMPFAIL) withbackup in progress (lock held since <ts>)on stderr. - A stale lock (mtime older than 5 minutes, left by a crashed run) is removed automatically by the next run.
Inspecting the scheduler
systemctl status purge-warden-backup.timer # when the next tick fires
systemctl status purge-warden-backup.service # last tick + exit code
journalctl -u purge-warden-backup.service # tick history (incl. "not due" / "disabled")
How restore works
Knowing the order of operations helps you reason about what’s safe and what isn’t.
- Extract. The archive is unpacked into a per-process staging
directory under
$TMPDIR. The staging directory is auto-cleaned on exit, including on failure. - Locate the master. The handler matches the file name of your live
master against the staged tree; if no match, it falls back to the first
*.tomlat the staging root. - Validate. The staged master is loaded through Warden’s full v1
loader (
loader::load_config) — same validator thatwarden config lintuses. If validation fails, the live tree is not touched. Exit code 1, errors printed with file and line. - Snapshot the live master. The current live master is renamed to
<live>.pre-restore-<UTC-timestamp>as a rollback path. - Atomic replace of the master. The staged master is written into
place via
atomic_write_and_validate— temp file in the same directory, followed by a POSIX rename. On crash mid-write, the live file is untouched. - Replace each
.d/sibling. For every*.d/directory present in the staged tree, the live one is removed and the staged one is copied recursively. This step is not atomic across directories — a crash between two siblings leaves a mix. - Signal the daemon. If the PID file is present, the handler sends
SIGHUP. The daemon reloads from disk viaArcSwap— no restart, no dropped queries. If the SIGHUP fails, the restore still succeeds and prints a warning; the nextwarden reload(or daemon start) picks up the new state.
Source: src/cli/commands/config/restore.rs.
*.d/ directories are removed and replaced wholesale. If you
have local slice files in devices.d/ or profiles.d/ that aren’t in the
archive, they will be deleted. If that is a concern, copy them aside
first, restore, then re-add.config.toml.pre-restore-<ts>), not the .d/ siblings. To roll a
restore fully back, restore from the previous archive, not from the
.pre-restore file.Exit codes
| Code | Meaning |
|---|---|
0 | Success — backup written, restore applied (SIGHUP delivered if applicable), or an --auto run that wasn’t due. |
1 | Restore validation failed (the live tree is unchanged, error names the file and key), or an I/O error: archive missing, unreadable, malformed tarball, or destination not writable. |
75 | EX_TEMPFAIL — a backup is already in progress (lock held). Retry shortly. |
Source: src/cli/commands/config/restore.rs, src/cli/commands/config/backup.rs.
Security
- Archives contain plaintext config, including any tokens or secrets you
have inlined into the TOML. Treat them as sensitive; don’t push them to a
public Git remote, and prefer
chmod 600on the destination directory. - No encryption. If you need encrypted off-host backup, wrap the
archive yourself:
gpg --encrypt,age, or roll the archive into a filesystem snapshot that is itself encrypted. - No signature. The restore handler trusts the archive content beyond the v1 validator. Don’t restore archives from untrusted sources.
- Audit trail. Backup and restore actions are recorded in the audit log with the invoking uid, the archive path, and the result.
Troubleshooting
warden config restore exits 1 with schema_version mismatch.
The archive is from a Warden version older than 1.x. Run
warden migrate v0-to-v1 against the extracted master before restoring,
or restore an archive taken by the same major version.
warden config restore exits 0 but the daemon still serves the old
config.
The SIGHUP was missed (typically because the PID file was stale or the
daemon had been restarted under a different PID). Run warden reload
manually; if that also reports “no daemon”, the daemon was not running
during the restore.
warden config backup exits 75 with backup in progress.
Another backup holds the lock — usually a scheduled --auto run overlapping
your manual one. Wait a moment and retry. If no backup is actually running,
a stale .lock older than 5 minutes is cleared on the next run.
Scheduled backups stopped happening.
The scheduler auto-disabled after repeated failures. Check
sudo cat <backup-dir>/.auto_state for "disabled": true and the
last_outcome message, fix the cause (usually directory permissions), then
re-enable with warden config backup --reset-auto-failure.
tar: Cannot open: Permission denied from warden config backup.
The backup directory does not exist or is not writable by the daemon user.
Create it with the right ownership (see where archives go)
or pick a writable --out.
Backup file size grows over time.
Most likely a .d/ directory has accumulated stale slice files. Run
warden config lint to spot orphans and clean them up before the next
backup.
warden config restore succeeded but a custom slice file is gone.
Restore replaces .d/ directories wholesale. The slice was not in the
archive. If you need it back, restore from an older archive that contains
it, or recreate it manually.
For other operational issues, see troubleshooting.
See also
warden configreference — every config verb at a glance.- Server globals — what lives in the master TOML.
- File paths — where Warden reads and writes on disk.
- systemd service — the timer and service units.
- Audit log — confirms each backup/restore.
- Troubleshooting — broader recovery flows.