One binary, one YAML file. It streams your files or your database through compression and encryption straight to S3, SFTP or a local disk — and then it checks that what landed can actually be read back.
--user installs to
$HOME/.local/bin without sudo.
Documentation.
Source to destination is a single io.Pipe
chain. A 7 GB database never becomes a 7 GB temp file waiting for the upload to finish.
files · postgres · mysql
streamed, never staged
level 3, configurable 1–22
3.5 GiB → 333 MiB
X25519, encryption required
refuses to ship plaintext
S3 · SFTP · local
atomic on all three
A pg_dump that dies halfway through
surfaces as a pipeline error, not as a silently truncated archive — the subprocess is
reaped on EOF and its exit code travels back up the read path.
The moment ransomware runs, your next backup is the thing that overwrites the last good one. Preflight samples the entropy of every file and compares it to a baseline written on the first run. Files that used to be text and are suddenly noise get flagged.
off ·
warn ·
block.
In block mode, crossing the threshold fails preflight with exit code 2 —
before a single byte ships. The last good backup stays the last good backup.
Low entropy is under 6.0, high is over 7.5, and only a path that moved from one to the
other counts. Already-compressed extensions are ignored. The baseline refreshes every 30
days, and rebaseline resets it after
a legitimate rewrite like a deploy or a migration.
A triggered run raises a critical incident and fires
backup.ransomware.suspected through the
normal notification pipeline — Slack, Discord, email, webhook, PagerDuty.
Every check runs even after one fails, so a misconfigured backup tells you everything that is wrong in one pass instead of one problem per attempt.
Source paths exist and are readable
Database credentials resolvable — env, or a 0600 password file
pg_dump / mysqldump on $PATH, and the server answers
Staging has the estimated source size × 1.2 free
Destination writable — proved with a probe write
Encryption recipient parses as a valid age key
Pre- and post-hook commands resolve on $PATH
Exit codes are distinct on purpose: 0 success, 1 pipeline failure, 2 preflight failure or lock collision — so a cron wrapper can tell a broken config from a broken run.
Writing bytes to a bucket proves the network worked. It does not prove the archive opens.
Opt out. The run finishes when the bytes are gone.
HEAD the object and compare ContentLength against what was shipped. Roughly 100 ms.
Pull the archive back, decrypt, decompress, then read it: tar entries counted, pg_dump TOC listed via pg_restore --list, mysqldump header checked. A failed verify errors the source and fires ?fail.
Deep verification needs the decryption identity on the host that runs it. Most backup-only hosts should not carry that key — a documented trade-off, not an oversight.
Fetches the newest archive, decrypts, decompresses and unpacks it.
--from targets a specific key,
--identity overrides the key file.
Tar extraction rejects absolute paths and ...
A Postgres archive lands as a .dump
and prints the pg_restore line to run.
MySQL lands as .sql with its
mysql line. Restores are measured:
333 MiB from R2 back to a 23 GB dump took two minutes.
No account, no sign-in, no network calls to HostAtlas. A YAML file and a cron entry. Run logs land on disk as JSON; the exit code tells your wrapper what happened.
Sign in once with login and each run
pings a heartbeat and posts its log to the dashboard.
Best effort by contract — if we are unreachable, your backup still succeeds and
still says so.
One binary. Encrypted by default, ransomware-aware, and honest about what it verified.