zrb — ZFS Remote Backup
zrb is a lightweight tool for pushing ZFS snapshots from a Source host (e.g. a laptop) to a Remote server over SSH. It
handles snapshot creation, incremental transfers, interrupted-transfer resumption, and snapshot pruning — all driven
from the Source side.
How it works
Source (laptop) ──SSH──► Remote (backup server)
zrb send zrb server (ForceCommand)
The Source creates a snapshot, opens an SSH connection, performs a structured handshake to negotiate an incremental
base, and streams the snapshot to the Remote via zfs send | zfs receive. The Remote never initiates contact. If a
transfer is interrupted mid-stream, the next zrb send resumes from where it left off using ZFS native resume tokens.
Requirements
- Linux with OpenZFS (
zfsandzpoolinPATH) - SSH access from Source to Remote
- Rust toolchain (for source builds) or Nix
Installation
From crates.io:
With Nix:
Setup
1. Generate an SSH key on the Source
2. Create a dedicated user on the Remote
Copy the public key to the Remote:
3. Configure SSH ForceCommand on the Remote
Edit /home/zfsbackup/.ssh/authorized_keys so that the key runs zrb server instead of a shell:
command="zrb server --client my-laptop",restrict ssh-ed25519 AAAA... zrb backup key
The --client flag lists which client names this key is permitted to present. A key may serve multiple clients:
--client laptop --client desktop.
4. Grant ZFS permissions on the Remote
Delegate only the necessary permissions to the zfsbackup user on the dataset subtree it will receive into:
Keep the delegation as narrow as possible — per-dataset subtree, not the whole pool.
5. Write the Remote config
~/.config/zrb/server.toml on the Remote:
[]
# Discard stale interrupted-transfer state after this many days.
= 3
[]
# Datasets this client is allowed to receive into.
= ["backup/laptop/home", "backup/laptop/documents"]
= ["-c"]
[]
= 14
= 60
= 730
6. Write the Source config
~/.config/zrb/config.toml on the Source (default path; override with --config):
[]
= "my-laptop"
[]
= "backup.example.com"
= 22
= "zfsbackup"
= "/home/user/.ssh/id_zrb"
= ["-o", "ServerAliveInterval=30"]
= ["-Lec"]
# Map each local dataset to its destination path on each remote.
# Key = local dataset, value = map of remote-name → remote dataset.
[]
= "backup/laptop/home"
[]
= "backup/laptop/documents"
[]
= 7
= 30
= 365
Usage
All subcommands accept --verbose for debug logging and --config <path> to override the default config location.
zrb send <dataset>...
Creates a snapshot, connects to all configured Remotes, and transfers incrementally.
# Send two datasets to all remotes
# Restrict to a single named remote
If the previous transfer was interrupted, zrb send resumes it automatically before starting new sends.
zrb snapshot <dataset>...
Creates a zrb--prefixed snapshot locally without transferring it.
Useful for taking a local checkpoint before a risky operation.
zrb list <dataset>
Lists all zrb-managed snapshots for a dataset.
zrb prune <dataset>
Deletes snapshots that fall outside the Retention Policy. Runs locally — Source and Remote prune independently.
zrb server --client <name>...
Runs in server mode. Intended to be called only by SSH ForceCommand, not directly.
Automating with systemd
Create a service and timer on the Source to run backups on a schedule.
/etc/systemd/system/zrb-send.service
[Unit]
Description=ZFS remote backup
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/zrb send tank/home tank/documents
# zrb notifies systemd on start and stop — useful with Type=notify if you
# want the timer to track active send duration instead of one-shot.
/etc/systemd/system/zrb-send.timer
[Unit]
Description=Run ZFS remote backup daily
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
Enable and start:
Check the last run:
NixOS
There are NixOS modules for easy integration of server and/or client.
Both modules are exposed as nixosModules.server and nixosModules.client from the flake.
Add to your flake.nix inputs:
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
zrb = {
url = "github:0xCCF4/zrb";
inputs.nixpkgs.follows = "nixpkgs";
};
};
Server
{
imports = [ inputs.zrb.nixosModules.server ];
services.zrb.server.main = {
enable = true;
retention = {
recent = 14;
weeklyForDays = 60;
monthlyForDays = 730;
};
clients.my-laptop = {
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... zrb backup key";
allow = [ "backup/laptop/home" "backup/laptop/documents" ];
};
};
}
The module creates the zrb system user, writes /etc/zrb/main/server.toml, and adds a ForceCommand-restricted entry
to the user's authorized_keys for each client key. You still need to grant ZFS permissions manually:
Client
{
imports = [ inputs.zrb.nixosModules.client ];
services.zrb.client = {
enable = true;
sourceName = "my-laptop";
remotes.primary = {
host = "backup.example.com";
user = "zrb";
sshKey = "/etc/zrb/id_ed25519";
};
datasets = {
"tank/home".primary = "backup/laptop/home";
"tank/documents".primary = "backup/laptop/documents";
};
retention = {
recent = 7;
weeklyForDays = 30;
monthlyForDays = 365;
};
jobs.nightly = {
onCalendar = "daily";
datasets = [ "tank/home" "tank/documents" ];
};
prune.onCalendar = "weekly";
};
}
The module creates the zrb system user, writes /etc/zrb/client.toml, and registers a zrb-send-nightly systemd
service+timer and a zrb-prune service+timer. The SSH key at sshKey must be provisioned separately (e.g. via
sops-nix or agenix).
Retention policy
The same [retention] block is used in both Source and Remote configs. Each host prunes independently.
| Window | Rule |
|---|---|
| Last N snapshots | Always kept (recent) |
Older, within weekly_for_days |
One per ISO week |
Older, within monthly_for_days |
One per calendar month |
| Older still | One per year |
Only snapshots with the zrb- prefix are managed. Any snapshots you created manually are left untouched.
Security model
Three independent layers protect the Remote:
- ZFS delegation (
zfs allow) — the OS enforces which datasets the backup user may write to, regardless of whatzrbdoes. This is the authoritative boundary. - SSH key → client name binding — each
authorized_keysentry restricts which client names may connect with that key. A compromised key cannot impersonate unlisted clients. - Server config allowlist —
[clients.<name>].allowmaps each client to its permitted receive targets, catching misconfiguration early with a clear error message.
Snapshot naming
All managed snapshots follow the pattern <dataset>@zrb-<ISO-8601-UTC>, e.g.:
tank/home@zrb-2026-05-22T14:30:00Z
zrb ignores all snapshots that do not match this prefix.
On the use of AI
This project was a long-standing concept and half finished prototype before any AI tools were used. When I recently tried Claude for the first time, I let it read the existing code and documentation, and in a supervised manner, used it to bring to project into a usable state. Note that every change was carefully reviewed and edited afterward by myself @0xCCF4.
License
Due to the use of AI in the development process, I cannot confidently assert that this project is free of "inspiration" from others' work, but I am not aware of any equivalent project, from which, AI might have copied major parts. Since the idea, starting code base, and so on where given by myself, I am for now releasing this project under the GPLv3 license into the open source community.
Contributions
Contributions are welcome! Please open an issue or submit a pull request.