EN

Backup & restore

Work in progress — content may be incomplete.

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 backup on the old box, copy the archive, run warden config restore on the new one. The daemon on the new host picks up the config without a restart.
  • Recovering from a bad edit. A warden config edit slipped 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 *.secret files.
  • 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.

Why config-only
Blocklists redownload themselves on the next refresh; statistics regenerate from live traffic; audit and query logs are append-only journals of past events that you generally do not want to overwrite. The config tree, on the other hand, is the one thing that is irreversibly your work — and the one thing where a bad edit can cost you DNS for the whole LAN. The backup scope reflects that asymmetry.

CLI

CommandWhat it does
warden config backupWrite <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 --listList the restore points in the backup dir (date, age, size, newest first). Restores nothing.
warden config restore --latestRestore 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:

config.toml
toml
[backup]
dir = "/srv/purge-backups"   # default: <config-parent>/backups

The 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:

/etc/cron.d/purge-warden-backup
cron
# 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 -delete

A 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, press Enter, confirm with y; Warden restores it and reloads the daemon. The previous config is kept alongside as a .pre-restore copy, 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:

config.toml
toml
[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.serviceType=oneshot, runs warden config backup --auto as the daemon user.

The timer does not know your interval. Each --auto run reads the state file and decides:

  1. disabled = true → exit 0.
  2. No auto_interval set → exit 0.
  3. now - last_attempt < auto_interval → exit 0 (“not due”).
  4. 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 --auto runs 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":"…"}.
  • disabledtrue after disable_after_failures consecutive 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) with backup 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.

  1. Extract. The archive is unpacked into a per-process staging directory under $TMPDIR. The staging directory is auto-cleaned on exit, including on failure.
  2. 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 *.toml at the staging root.
  3. Validate. The staged master is loaded through Warden’s full v1 loader (loader::load_config) — same validator that warden config lint uses. If validation fails, the live tree is not touched. Exit code 1, errors printed with file and line.
  4. Snapshot the live master. The current live master is renamed to <live>.pre-restore-<UTC-timestamp> as a rollback path.
  5. 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.
  6. 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.
  7. Signal the daemon. If the PID file is present, the handler sends SIGHUP. The daemon reloads from disk via ArcSwap — no restart, no dropped queries. If the SIGHUP fails, the restore still succeeds and prints a warning; the next warden reload (or daemon start) picks up the new state.

Source: src/cli/commands/config/restore.rs.

Restore replaces, doesn't merge
The live *.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.
Rolling back a restore
The pre-restore snapshot covers only the master TOML (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

CodeMeaning
0Success — backup written, restore applied (SIGHUP delivered if applicable), or an --auto run that wasn’t due.
1Restore 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.
75EX_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 600 on 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