Security model¶
| Promise | Enforced by |
|---|---|
| Joe cannot touch your datasets | delegation exists only on his root; receive, destroy, send, set, and allow anywhere else fail with permission denied in the kernel |
| Joe cannot delete or reconfigure his root | the root only delegates create,mount,receive locally; destroy, snapshot, set quota, and allow on it are denied |
| Joe cannot see your datasets | the gate rejects any name outside his root before calling zfs, allowlists which properties zfs list may show, and answers syncoid's ps and command -v probes with nothing |
| ...even if the gate had a bug | everything runs inside Joe's zone, where the kernel hides every dataset that is not his; with zoned=on, his delegated rights only work from inside that zone |
| Joe cannot store more than you agreed | quota on the root, set by root |
| Joe cannot flood you with datasets or snapshots | filesystem_limit and snapshot_limit, which OpenZFS enforces for exactly this kind of delegated user |
| Joe's SSH gate cannot consume unbounded receiver userspace | on NixOS, its user scope runs in a tenant slice with memory, task, and CPU limits; PAM also limits the account's process count |
| You cannot read Joe's data | raw sends from Joe's side; after a successful receive, the gate destroys any new unencrypted dataset and fails the push (an interrupted receive skips that cleanup), which exposes a misconfigured sender but cannot unsend the plaintext |
| Nothing of Joe's ever gets mounted or shared on your machine | zoned=on (the host never mounts zoned datasets, so it never shares them), plus mountpoint=none, canmount=off, readonly=on, exec=off, setuid=off, devices=off, volmode=none on the root; the gate always receives with -u; property overrides inside a stream fail with permission denied |
| Joe's key cannot run anything else | the forced command; the gate never uses a shell |
Every row is exercised by a two-node NixOS VM test with real OpenZFS and real syncoid (nix/integration-test.nix).
What it cannot hide¶
OpenZFS encryption protects file contents, not structure.
From zfs-load-key(8): "ZFS will not encrypt metadata related to the pool structure, including dataset and snapshot names, dataset hierarchy, properties, file size, file holes, and deduplication tables."
So you, as the host, can see the names, sizes, and snapshot times of Joe's datasets.
Give datasets you send to a friend boring names, and never send with -p or -R, which would include properties.
You are also root on your own machine. You can always delete Joe's backup copy, even though you can never read it.
Residual risks¶
- The kernel parses the send streams Joe sends you. That is the same exposure as any ZFS replication.
- A leaked tenant key lets someone push data up to the quota and delete that tenant's backups.
- The NixOS user slice limits the restricted SSH gate path, not arbitrary code running as the tenant account: such code could change its own user manager. The PAM process limit helps bound processes outside the slice. These controls do not bound all ZFS kernel memory.
- A failed or interrupted receive may leave plaintext or partial receive state. Inspect and clean up the affected dataset after any failed push; do not assume the gate's successful-receive encryption check ran.
- Inside a zone, pool-level information (
zpool list,zpool status) and the parent datasets' sizes are still visible tozfs. The gate does not allow those commands; the zone only matters if the gate is bypassed. - syncoid 2.3.0 pastes the resume token it gets from the receiving host into a shell on the sending machine without escaping it, so a malicious host could run commands on the sender as the user running syncoid.
So run syncoid as a dedicated user that holds only
sendandholdrights on the datasets it pushes. The NixOS example below does this withservices.syncoid; by hand, usezfs allow -u <user> send,hold <dataset>.