clevis-zfs

Tang client + ZFS native encryption driver for FreeBSD: unattended boot-time unlock of encrypted ZFS datasets via NBDE. Successor to crystal-clevis-geli.

= clevis-zfs :toc: left :toclevels: 2 :source-highlighter: rouge

image:https://github.com/aloli-crystal/clevis-zfs/actions/workflows/ci.yml/badge.svg[CI,link=https://github.com/aloli-crystal/clevis-zfs/actions/workflows/ci.yml]

Tang client + ZFS native encryption driver for FreeBSD: unattended boot-time unlock of encrypted ZFS datasets via the Tang protocol (Network-Bound Disk Encryption — NBDE).

Successor to https://github.com/aloli-crystal/crystal-clevis-geli[crystal-clevis-geli], using ZFS native encryption instead of GELI to benefit from pre-encryption compression and zfs send -w portable encrypted backups.

French version: link:README.fr.adoc[README.fr.adoc].

== Why ZFS native encryption (and not GELI)?

  • Performance: ZFS compression runs before encryption. Compressible payloads (logs, databases, JSON, source code) shrink by 2-5× before being encrypted, so I/O and CPU are both saved. With GELI, ZFS sees cryptographic noise and compression becomes useless.
  • Boot simplicity: a single zpool import + zfs load-key instead of a chain of geli attach × N + zpool import × N.
  • Portable backups: zfs send -w exports encrypted snapshots as-is, so off-site backups can flow through untrusted relays without ever exposing the key.

== Architecture

[source]

              ┌────────────────────────────────────┐
              │     clevis-zfs (this)      │
              │                                    │
              │  bind / unlock  ── zfs load-key    │
              │  Tang HTTP/JOSE                    │
              └─────────────┬──────────────────────┘
                            │
                            │ HTTP + JOSE
                            ▼
                   ┌────────────────┐
                   │ Tang server(s) │
                   │ (FreeBSD pkg)  │
                   └────────────────┘

== Usage

[source,shell]

Bind a ZFS dataset to a Tang server (creates the encrypted dataset)

clevis-zfs bind --init
--dataset zroot/zsys
--tang http://tang-nas.local:8888

At boot (called from rc.d):

clevis-zfs unlock --dataset zroot/zsys

== Status

  • v0.1: single-Tang bind/unlock for one ZFS dataset. Tang protocol on top of jose. JOSE primitives validated against latchset/jose.
  • v0.1: ZFS native encryption (keyformat=hex, keylocation=prompt via stdin, never on argv).
  • v0.1: rc.d for unattended boot-time unlocking (after NETWORKING, before mountcritremote).
  • v0.2 ✅: CLI for multi-Tang Shamir Secret Sharing (--tang URL repeatable, --threshold K). The unlock subcommand auto-detects single-Tang vs SSS from the JWE header.
  • v0.2 ✅: bind --use-existing-key for migrating an already-encrypted dataset (mode: ssh_unlockmode: tang) without rewriting the data.
  • v0.2 ✅: handles all three RFC 7515 JWS serializations for Tang advertisements (Compact, Flattened JSON, General JSON with signatures[] array — emitted by FreeBSD tangd after tangd-rotate-keys).

== Tested in a real FreeBSD VM

The repository ships a scripted QEMU lab (macOS Apple Silicon) under link:examples/qemu-lab/[examples/qemu-lab/]. Two FreeBSD 15 aarch64 VMs (Tang server + ZFS client), eight numbered shell scripts, ~10 minutes from cold start to a fully validated end-to-end.

The lab covers:

[cols="2,3", options="header"] |=== | Scenario | Expected outcome

| Single-Tang bind --init + unlock | Encrypted ZFS dataset (AES-256-GCM, lz4) created and reattached after zfs unload-key.

| Multi-Tang K=2/N=3 bind + unlock | JWE carries clevis.sss claim with 3 sub-JWE; unlock with all Tangs reachable.

| Tolerance: kill 1 of 3 Tangs | unlock still succeeds.

| Failure: kill 2 of 3 Tangs | unlock fails cleanly with recovered only 1 / 2 required shares.

| Reboot the VM | rc.d/clevis-zfs auto-unlocks both datasets within ~20 seconds; child datasets mount automatically; I/O works. |===

To run it yourself:

[source,shell]

cd examples/qemu-lab/ ./00-fetch-image.sh # downloads the FreeBSD 15 cloud image ./01-prepare-disks.sh # clones disks, generates cloud-init seeds ./10-run-tang.sh # boots the Tang VM ./11-run-client.sh # boots the ZFS client VM ./20-provision-tang.sh # installs tang, runs 3 tangd instances ./21-provision-client.sh # builds + installs the binary ./30-test-bind-unlock.sh # 4 test scenarios, exits non-zero on any failure ./99-stop-all.sh # clean shutdown

== Installation

[source,shell]

shards install crystal build src/cli.cr -o clevis-zfs --release sudo install -m 0755 clevis-zfs /usr/local/sbin/ sudo install -m 0755 etc/rc.d/clevis-zfs /usr/local/etc/rc.d/

Then in /etc/rc.conf:

[source,shell]

crystal_clevis_zfs_enable="YES" crystal_clevis_zfs_datasets="zroot/zsys zroot/zdata zroot/zsave"

== Development

[source,shell]

shards install crystal spec crystal tool format --check bin/ameba

== Key handling discipline

  • The key is never passed via command-line arguments (would leak through ps).
  • The key is never set in the environment of a child process.
  • The key is fed to zfs(8) via stdin, then the pipe is closed.
  • The Crystal String carrying the key is scrubbed (overwritten with zeros) before being released to the GC.
  • The JWE on disk does not contain the key in clear: it only contains the McCallum-Relyea envelope, which can be unwrapped only by collaborating with the Tang server.

== License

MIT — see link:LICENSE[LICENSE].

== References

Repository

clevis-zfs

Owner
Statistic
  • 0
  • 0
  • 0
  • 0
  • 2
  • about 3 hours ago
  • April 27, 2026
License

MIT License

Links
Synced at

Wed, 29 Apr 2026 09:56:09 GMT

Languages