clevis-zfs
= clevis-zfs :toc: left :toclevels: 2 :source-highlighter: rouge
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-keyinstead of a chain ofgeli attach × N+zpool import × N. - Portable backups:
zfs send -wexports 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 againstlatchset/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, beforemountcritremote). - v0.2 ✅: CLI for multi-Tang Shamir Secret Sharing (
--tang URLrepeatable,--threshold K). Theunlocksubcommand auto-detects single-Tang vs SSS from the JWE header. - v0.2 ✅:
bind --use-existing-keyfor migrating an already-encrypted dataset (mode: ssh_unlock→mode: 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 FreeBSDtangdaftertangd-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
Stringcarrying 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
- https://github.com/latchset/clevis[Clevis] — the Linux/LUKS reference (
pin: sss) - https://github.com/latchset/tang[Tang] — the server side
- https://www.freebsd.org/cgi/man.cgi?zfsprops(7)[zfsprops(7)] — ZFS native encryption properties
- https://github.com/aloli-crystal/jose[jose] — JOSE primitives in pure Crystal
clevis-zfs
- 0
- 0
- 0
- 0
- 2
- about 3 hours ago
- April 27, 2026
MIT License
Wed, 29 Apr 2026 09:56:09 GMT