Skip to content

Atomically change ZFS encryption keys with user properties via channel programs

License

Notifications You must be signed in to change notification settings

oxidecomputer/zfs-atomic-change-key

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

59 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

zfs-atomic-change-key

Atomically change ZFS encryption keys with user properties via channel programs.

Overview

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).

Motivation

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.

Usage

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()

Key-only rotation

// Without properties, uses direct `zfs change-key` (faster, no channel program)
dataset.change_key(Key::hex([0xAA; 32])).await?;

How It Works

With properties, the crate generates a Lua channel program that executes atomically in ZFS kernel context:

  1. Read current property values (for rollback)
  2. Set all properties to new values
  3. Change the encryption key (point of no return)
  4. 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.

Security

  • Keys are never passed as command-line arguments (invisible to ps)
  • Key material is embedded in Lua scripts piped via stdin
  • [Key] uses zeroize to securely clear memory on drop
  • Clone is intentionally not derived to prevent accidental duplication
  • All inputs are validated to prevent Lua injection

Validation Limits

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

Requirements

  • ZFS with channel program support (illumos, ZFS 0.7.0+)
  • Root privileges (pfexec on illumos)
  • Dataset must be an encryption root with key loaded

Testing

cargo test                              # Unit tests
cargo test -- --ignored                 # Integration tests (requires ZFS + root)

License

MPL-2.0

About

Atomically change ZFS encryption keys with user properties via channel programs

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages