Atomically change ZFS encryption keys with user properties via channel programs.
This crate provides a secure, atomic way to rotate ZFS dataset encryption keys
while simultaneously updating user properties (like oxide:epoch for key
versioning). It uses ZFS channel programs to ensure all changes occur within a
single transaction group (txg).
Sometimes, on ZFS, you want to keep track of metadata about the key you're using
to encrypt a given dataset. ZFS provides user properties for storing this
metadata, but zfs change-key can only update encryption-specific properties
(keyformat, keylocation, pbkdf2iters) whilst rotating keys. Updating
custom properties like myapp:key_epoch requires a separate zfs set command,
creating a race condition:
zfs change-key -o keyformat=raw tank/data < /path/to/new.key
# ← crash here leaves epoch stale
zfs set myapp:key_epoch=2 tank/data
If the system fails between these commands, your metadata no longer reflects reality, regardless of which way you choose to order them.
This crate eliminates that window of failure by executing key rotation and property updates atomically within a single ZFS transaction group. Either both succeed together, or neither does.
use zfs_atomic_change_key::{Dataset, Key};
async fn rotate_key() -> Result<(), Box<dyn std::error::Error>> {
let dataset = Dataset::new("tank/secure-data")?;
// Rotate the key atomically with property updates
dataset.change_key(Key::hex([0x01; 32]))
.property("oxide:epoch", "2")
.property("oxide:rotated_at", "2024-01-15T12:00:00Z")
.await?;
Ok(())
}The fluent builder API supports:
- Multiple properties: chain
.property()calls or use.properties() - Key formats:
Key::hex()(64 hex chars),Key::raw()(32 bytes),Key::passphrase() - Options:
.keylocation(),.pbkdf2iters()(passphrase only),.load_key_first()
// Without properties, uses direct `zfs change-key` (faster, no channel program)
dataset.change_key(Key::hex([0xAA; 32])).await?;With properties, the crate generates a Lua channel program that executes atomically in ZFS kernel context:
- Read current property values (for rollback)
- Set all properties to new values
- Change the encryption key (point of no return)
- Return success
If any step fails before the key change succeeds, properties are rolled back to their original values.
Without properties, the crate calls zfs change-key directly.
- Keys are never passed as command-line arguments (invisible to
ps) - Key material is embedded in Lua scripts piped via stdin
- [
Key] useszeroizeto securely clear memory on drop Cloneis intentionally not derived to prevent accidental duplication- All inputs are validated to prevent Lua injection
| Input | Constraint |
|---|---|
| Dataset name | 255 bytes max, no @ or #, alphanumeric + /_-.: |
| Property name | 247 bytes max, namespace:property format |
| Property value | 8191 bytes max, no quotes/backslashes/brackets/control chars |
| Property count | 4096 max per operation |
- ZFS with channel program support (illumos, ZFS 0.7.0+)
- Root privileges (
pfexecon illumos) - Dataset must be an encryption root with key loaded
cargo test # Unit tests
cargo test -- --ignored # Integration tests (requires ZFS + root)MPL-2.0