Skip to content

Add support for ZFS encryption - #467

Open
lowjoel wants to merge 5 commits into
latchset:masterfrom
lowjoel:add-zfs-support
Open

lowjoel wants to merge 5 commits into
latchset:masterfrom
lowjoel:add-zfs-support

Conversation

@lowjoel

@lowjoel lowjoel commented Jun 12, 2024 •

Copy link
Copy Markdown

Supersedes #373. Description copied:

Why

As I stated a while ago in #218, I would like clevis to be able to unlock ZFS datasets that have native encryption enabled. This is my attempt at adding this by storing the data in zfs properties.

How

This is achieved by storing the clevis data (output of clevis-encrypt) in ZFS User Properties. From zfsprops(8):

   User Properties
     In addition to the standard native properties, ZFS supports arbitrary user properties.  User
     properties have no effect on ZFS behavior, but applications or administrators can use them
     to annotate datasets (file systems, volumes, and snapshots).

     User property names must contain a colon (":") character to distinguish them from native
     properties.  They may contain lowercase letters, numbers, and the following punctuation
     characters: colon (":"), dash ("-"), period ("."), and underscore ("_").  The expected
     convention is that the property name is divided into two portions such as module:property,
     but this namespace is not enforced by ZFS.  User property names can be at most 256
     characters, and cannot begin with a dash ("-").

     When making programmatic use of user properties, it is strongly suggested to use a reversed
     DNS domain name for the module component of property names to reduce the chance that two
     independently-developed packages use the same property name for different purposes.

     The values of user properties are arbitrary strings, are always inherited, and are never
     validated.  All of the commands that operate on properties (zfs list, zfs get, zfs set, and
     so forth) can be used to manipulate both native properties and user properties.  Use the zfs
     inherit command to clear a user property.  If the property is not defined in any parent
     dataset, it is removed entirely.  Property values are limited to 8192 bytes.

Properties

All clevis user properties are prefixed with latchset.clevis

* one property to check if a dataset is bound: `latchset.clevis:labels`, should be a space-separated list of bound labels.  or absent when unbound.

* one or more properties to store the clevis data: `latchset.clevis.label:LABEL_NAME[-N]` where `-N` is an integer suffix when the data for label LABEL_NAME is too large for one property.

If there are more than 10 properties needed, the integer will be 0-padded to help with sorting to easily combine them when unlocking.

As noted above (at the end), the limit of the value of a user property is 8192 bytes. A simple 1-host tang setup will probably not go over this limit, but with a more complicated setup with clevis-encrypt-sss, it is possible.

Because of that, the clevis data is split in 8k chunks, and saved in multiple user-properties. These are combined upon unlock.

"Works": (works on my machine, needs more testing)

* binding ZFS dataset with `clevis-zfs-bind`

* unbinding ZFS dataset with `clevis-zfs-unbind`

* testing and unlocking ZFS dataset with `clevis-zfs-unlock`

* splitting and combining zfs-properties (tested with a limit of 800 instead of 8000)

To Do:

* [ ]  manpages

* [ ]  initramfs hooks

* [ ]  rebinding support? (like clevis-luks-rebind)

* [x]  Maybe: multiple "slots" support. Currently only one "slot" is available. Added label support

* [x]  clean up commits if this is not squashed

Further work by @lowjoel:

  • Cleaned up the error messages, fixed lints reported by shellcheck
  • Completed support for initramfs. This requires OpenZFS 2.2+ because of openzfs/zfs@6e01593. Tested on Ubuntu 24.04.
  • Tested Dracut on Ubuntu 26.04.
  • Did a full clevis-zfs-bind, clevis-zfs-unlock (at reboot), and clevis-zfs-unbind
  • PPA available for Ubuntu 22.04
  • Integrate clevis-zfs-test with Meson

Once we're happy with the code I can squash the commits to those by @techhazard and myself.

@lowjoel lowjoel mentioned this pull request Jun 12, 2024
1 of 5 tasks
@lowjoel
lowjoel force-pushed the add-zfs-support branch 5 times, most recently from 59a2544 to 97bf779 Compare June 14, 2024 14:15
@almereyda

Copy link
Copy Markdown

Would there be any instructions, how this can be tested?

I'd like to aid in this implementation, but am unsure how.

Is there something that can be done by third-parties?

@lowjoel

lowjoel commented Jul 1, 2024

Copy link
Copy Markdown
Author

Would there be any instructions, how this can be tested?

I've packaged my changes for Ubuntu in a PPA. After you install that, it's essentially just using clevis-zfs-bind with the same Clevis configuration parameters. The package already triggers initramfs to be regenerated automatically.

I'd like to aid in this implementation, but am unsure how.

Testing would be good. But I only have this specific initramfs configuration.

@almereyda

almereyda commented Jul 2, 2024 •

Copy link
Copy Markdown

Thank you for the quick reply. I have found the PPA. Unfortunately I have never used Clevis and cannot unpack the sentence

just using clevis-zfs-bind with the same Clevis configuration parameters

to infer what a test setup would look like.

I'm able to set up an LXD/Incus virtual network that contains an Ubuntu VM/system container and a block devices that contains an encrypted ZFS pool plus a separate instance for Clevis.

As a tester I would ask myself:

  • Which are the steps to set up the Clevis server to provide valid ZFS decryption keys?
  • Which are the steps to set up the Ubuntu client to pull the encryption keys in initramfs?

I am having the intuition that bind, unlock and unbind are the hooks that make this work, but I am left asking myself how.

Would it seem suitable to add some documentation with this cycle? The information could go to the clevis.1.adoc man page, a new one like clevis-zfs.1.doc or the existing clevis-decrypt.1.adoc, plus into the README.md or INSTALL.md. This would allow to assure that the presence of the feature is also reflected in documentation.

@lowjoel

lowjoel commented Jul 2, 2024 •

Copy link
Copy Markdown
Author

to infer what a test setup would look like.

Ah, OK. Have a look at the Arch wiki. In my case, I want to bind the key with both my TPM and with a Yubikey, so I used clevis-zfs-bind -d rpool -l boot sss '{"t": 2, "pins": {"tpm2": {"pcr_ids": "0"}, "yubikey": {"challenge_size": 64, "salt_size": 64}}}'.

* Which are the steps to set up the Clevis server to provide valid ZFS decryption keys?

If you want a server, I think you're referring to Tang. I haven't set that up; my key unlock is purely held on the TPM and with a Yubikey, there's no network involved in my setup.

* Which are the steps to set up the Ubuntu client to pull the encryption keys in initramfs?

I am having the intuition that bind, unlock and unbind are the hooks that make this work, but I am left asking myself how.

Essentially Clevis does most of the heavy lifting. All this PR does is teach Clevis how to unlock ZFS datasets, which is why there isn't significant documentation. The two arguments:

  • -d tells Clevis which dataset you want to unlock. In my case, rpool (ZFS on Root)
  • -l tells Clevis where to store the actual ciphertext containing the key to unlock the ZFS dataset. I used the label boot which you can verify using zfs get latchset.clevis.label:boot rpool

clevis-zfs-unlock is called at initramfs to essentially scan for labels and try unlocking the datasets. clevis-zfs-unbind just removes the labels from the dataset.

Would it seem suitable to add some documentation with this cycle? The information could go to the clevis.1.adoc man page, a new one like clevis-zfs.1.doc or the existing clevis-decrypt.1.adoc, plus into the README.md or INSTALL.md. This would allow to assure that the presence of the feature is also reflected in documentation.

Calling clevis-zfs-bind --help also prints that documentation but I should figure that out. I'd like some confirmation that it works for someone other than me before I go write it 😅

@almereyda

Copy link
Copy Markdown

I'm on it, but give a few months. Thanks for the link and for helping me understand the relationship between Tang and Clevis better.

@lowjoel
lowjoel force-pushed the add-zfs-support branch 2 times, most recently from fc9e6a6 to 4317bd7 Compare September 29, 2024 09:06
@r4nc0r

r4nc0r commented Oct 22, 2024

Copy link
Copy Markdown

this would be a huge help for our organization 👍

@ylabidi

ylabidi commented Feb 18, 2025

Copy link
Copy Markdown

@lowjoel any help you need to wrap this pr up ?

@lowjoel

lowjoel commented Feb 18, 2025

Copy link
Copy Markdown
Author

More testing and code review is good. I've been using it for half a year now, but works well for me

@deatharse

Copy link
Copy Markdown

@lowjoel As you are requesting more testing I will be playing around with this shortly. Not an Ubuntu user but Debian. My plain is PVE on encrypted ZFS mirror. I have taken a look at your .debs and other than your packages the requirements and versions are the same as on Debian so I should have no problem using them. As there will be no LUKS on the intended test system I should be alright with only your clevis, clevis-zfs, and clevis-initramfs-zfs packages with versions 21-1202409290907ubuntu24.04.1. Do you envisage I would need more of your packages? My planned steps would be along the lines of:

# apt install libjose0 cracklib-runtime jose libcrack2
# dpkg -i clevis_21-1~202409290907~ubuntu24.04.1_amd64.deb clevis-zfs_21-1~202409290907~ubuntu24.04.1_amd64.deb clevis-initramfs-zfs_21-1~202409290907~ubuntu24.04.1_amd64.deb
# clevis-zfs-bind -d rpool -l boot tang '{"url": "http://tang.server.ip/"}'

@lowjoel

lowjoel commented Aug 12, 2025

Copy link
Copy Markdown
Author

@deatharse that looks about right! The changes here are written in shell so you probably could get away with just installing your distro clevis, and then just manually install using dpkg clevis-zfs and clevis-initramfs-zfs

@deatharse

Copy link
Copy Markdown

Thanks, I will let you know how I get on as it would be really nice to get this merged.

you probably could get away with just installing your distro clevis, and then just manually install using dpkg clevis-zfs and clevis-initramfs-zfs

Not so as clevis-zfs has a hard dependency on your version

Depends: clevis (= 21-1202409290907ubuntu24.04.1), zfsutils-linux

Which isnt a problem as the Debian version hasn't been updated to 21 yet anyway.

@deatharse

Copy link
Copy Markdown

@lowjoel So I've had a play. Much like my earlier suggested plan I:

  • installed PVE on a single disk with ZFS and converted it to an encrypted mirror
  • Installed your packages:
    • clevis_21-1202409290907ubuntu24.04.1_amd64.deb
    • clevis-initramfs-zfs_21-1202409290907ubuntu24.04.1_amd64.deb
    • clevis-zfs_21-1202409290907ubuntu24.04.1_amd64.deb
  • ran clevis-zfs-bind -d rpool -l boot01 tang '{"url": "http://tang.server.ip/"}'
  • Accepted the key
  • ran update-initramfs -u -k all
  • rebooted

Now I'm not sure if the following is due to PVE or not but upon reboot i recieved:

Error communicating with server http://tang.server.ip/
Key load error: encryption failure
Enter passphrase for 'rpool':

Okay, so I configured initramfs by adding a file /etc/initramfs-tools/conf.d/ip.conf with contents in the form:

IP={{ ip }}::{{ gateway }}:{{ netmask }}:{{ hostname }}:{{ interface }}:off

Updated initramfs again and rebooted.

Same thing occurred, however after entering the passphrase I saw the network configured output so it seemed like it was processing out-of-order. I decided to install and configure dropbear using only the minimal required packages:

  • dropbear-bin
  • dropbear-initramfs
  • libtomcrypt1
  • libtommath1

Configured dropbear, updated initramfs again, and rebooted. Success, unlocked using the tang server.

I know its advisable but I do not remember needing the additional steps with LUKS, but was likely just Debian. I will have a further play later by setting up Debian with ZFS on root.

I am not an "official reviewer" for the project but one thing i would suggest is in keeping consistency with clevis-luks-bind, your clevis-zfs-bind should implement the -y parameter to "Automatically answer yes for all questions". I know this is not necessary from an unattended point of view as you pass the fingerprint as part of the tang config json however for the sake of consistency I would implement it.

I hope this is helpful.

@deatharse

Copy link
Copy Markdown

@lowjoel Another consistency point @sarroutbi or @sergio-correia would probably pick up on as a nit-pick is the needless use of the function keyword in your bash scripts,

@deatharse

Copy link
Copy Markdown

@lowjoel one bug? I have noticed is if you pass the -f flag "Do not prompt when overwriting configuration" and there is no current config (i.e. the label does not currently exist) the clevis zfs bind command will exit with a status code of 1 and the message:

/usr/libexec/clevis-zfs-common: line 135: new_labels: unbound variable

it seems counter intuitive to me that this flag would cause it to fail in this circumstance.

@deatharse

Copy link
Copy Markdown

From further investigation the need for properly configured dropbear to bring up the network is due to clevis-initramfs-zfs_21-1~202409290907~ubuntu24.04.1_amd64.deb missing those capabilities. The normal clevis-initramfs has the files:

  • /usr/share/initramfs-tools/scripts/local-top/clevis
  • /usr/share/initramfs-tools/scripts/local-bottom/clevis

which from the file /usr/share/initramfs-tools/scripts/local-top/clevis the main clevisloop() invokes do_configure_networking() via

155        if [ $netcfg_attempted -eq 0 ] && has_tang_pin ${CRYPTTAB_SOURCE}; then
156            netcfg_attempted=1
157            do_configure_networking
158        fi

where there is obvious LUKS dependencies

108 has_tang_pin() {
109     local dev="$1"
110
111     clevis luks list -d "${dev}" | grep -q tang
112 }

Replicating both of these files and modifying the bottom of /usr/share/initramfs-tools/scripts/local-top/clevis to be:

- clevisloop &
- echo $! >/run/clevis.pid
+ do_configure_networking

Allows it to work as expected after updating initramfs.

It might be worthwhile abstracting the networking functions into a clevis-initramfs-common library both clevis-initramfs and clevis-initramfs-zfs can use if the project maintainers would allow it.

@deatharse

Copy link
Copy Markdown

So I've found an issue with unbinding.

If you do not use sss and bind 2 tang servers to two labels e.g.

# clevis zfs bind -d rpool -l boot01 tang '{"url": "http://192.168.1.2/"}'
# clevis zfs bind -d rpool -l boot02 tang '{"url": "http://192.168.1.3/"}'

You end up with 3 labels in the form:

# zfs get all rpool | tail -3
rpool  latchset.clevis.label:boot01 [tang server 1 data] local
rpool  latchset.clevis:labels           boot01 boot02         local
rpool  latchset.clevis.label:boot02 [tang server 2 data] local 

you can then unbind them in the reverse order:

# clevis zfs unbind -l boot02 -d rpool
Loading existing key... 
Enter existing ZFS password for rpool: 
Wiping Clevis data... ok

unbinds boot02 removes the label and unsets the boot02 from latchset.clevis:labels

# clevis zfs unbind -l boot01 -d rpool
Loading existing key... 
Enter existing ZFS password for rpool: 
Wiping Clevis data... ok

unbinds boot01 removes the label and latchset.clevis:labels

however if you unbind in sequential order boot01 then boot02

# clevis zfs unbind -l boot01 -d rpool
Loading existing key... 
Enter existing ZFS password for rpool: 
Wiping Clevis data... ok

removes the label and latchset.clevis:labels

therefor attempting

# clevis zfs unbind -l boot02 -d rpool
ERROR: dataset is not bound with Clevis: rpool
Usage: clevis zfs unbind [-f] [-k KEY] -d DATASET [-a] -l LABEL

Unbinds a label from a ZFS dataset:

  -f          Force unbinding dataset
  -d DATASET  The ZFS dataset on which to perform unbinding
  -a          Unbind all labels
  -l LABEL    The label to unbind

  -k KEY      Non-interactively read ZFS password from KEY file
  -k -        Non-interactively read ZFS password from standard input

the latchset.clevis.label:boot02 still exists and can only be removed via

# zfs inherit latchset.clevis.label:boot02 rpool

if having both bound you attempt to unbind all:

# clevis zfs unbind -a -d rpool

removes the label for boot01 and latchset.clevis:labels leaving the latchset.clevis.label:boot02 behind as though you have just attempted to unbind boot01

@lowjoel

lowjoel commented Aug 20, 2025

Copy link
Copy Markdown
Author

Thanks for this @deatharse -- you've given lots of good observations. Let me find a weekend to address them.

@deatharse

Copy link
Copy Markdown

I've had a bit of a further play by creating a second encrypted pool and noticed that was not unlocked.

# zpool create \
   -o ashift=12 \
   -o autotrim=on \
   -O encryption=on -O keylocation=prompt -O keyformat=passphrase \
   -O acltype=posixacl -O xattr=sa -O dnodesize=auto \
   -O compression=zstd \
   -O normalization=formD \
   -O relatime=on \
   -O canmount=off \
   -O recordsize=1M storage-pool /dev/vdc
# zfs create storage-pool/backups

I notice src/initramfs-tools/scripts/zfs-load-key/clevis-zfs.in is copied to /etc/zfs/initramfs-tools-load-key.d/clevis-zfs so started playing around with what was available via my dropbear connection and making some local modifications to see what I could figure out (I have no idea how the variable ENCRYPTIONROOT is set but carried on). I have drawn inspiration from the file dracut/60clevis-zfs/clevis-zfs-hook.sh and from the main repo src/luks/dracut/clevis/clevis-luks-unlocker to come up with a diff to allow unlocking non-root pools.

The diff for src/initramfs-tools/scripts/zfs-load-key/clevis-zfs.in is currently:

22c22,41
< clevis zfs unlock -d "${ENCRYPTIONROOT}"
---
> zpool import -a
> 
> for pool in $(zpool list -H -o name); do
>     # if pool encryption is active and the zfs command understands '-o encryption'
>     if [ "$(zpool list -H -o feature@encryption ${pool})" = 'active' ]; then
>         # if the root dataset has encryption enabled
>         ENCRYPTIONROOT=$(zfs get -H -o value encryptionroot "${pool}")
>         if ! [ "${ENCRYPTIONROOT}" = "-" ]; then
>             KEYSTATUS="$(zfs get -H -o value keystatus "${ENCRYPTIONROOT}")"
>             # continue only if the key needs to be loaded
>             [ "$KEYSTATUS" = "unavailable" ] || continue
>             # decrypt them
>             TRY_COUNT=5
>             while [ $TRY_COUNT -gt 0 ]; do
>                 clevis zfs unlock -d "${ENCRYPTIONROOT}" && break
>                 TRY_COUNT=$((TRY_COUNT - 1))
>             done
>         fi
>     fi
> done

N.B. I have yet to create an unencrypted pool with an encrypted dataset, maybe I will get round to that on a weekend.

@deatharse

Copy link
Copy Markdown

Heres an updated diff for src/initramfs-tools/scripts/zfs-load-key/clevis-zfs.in that will handle encrypted datasets in an unencrypted pool:

22c22,58
< clevis zfs unlock -d "${ENCRYPTIONROOT}"
---
> attempt_unlock() {
>     local dataset=$1
> 
>     KEYSTATUS=$(zfs get -H -o value keystatus "${dataset}")
>     # continue only if the key needs to be loaded
>     [ "$KEYSTATUS" = "unavailable" ] || break
>     # decrypt them
>     TRY_COUNT=5
>     while [ $TRY_COUNT -gt 0 ]; do
>         clevis zfs unlock -d "${dataset}" && break
>         TRY_COUNT=$((TRY_COUNT - 1))
>     done
> }
> 
> zpool import -a
> for pool in $(zpool list -H -o name); do
>     # if pool encryption is active and the zfs command understands '-o encryption'
>     if [ $(zpool list -H -o feature@encryption "${pool}") = 'active' ]; then
>         # if the root dataset has encryption enabled
>         ENCRYPTIONROOT=$(zfs get -H -o value encryptionroot "${pool}")
>         if ! [ "${ENCRYPTIONROOT}" = "-" ]; then
>             attempt_unlock "${ENCRYPTIONROOT}"
>         else
>             # encryption in child dataset, lets get list of datasets in pool
>             for dataset in $( zfs list -r -H -o name "${pool}" ); do
>                 # first entry will not be a child so we will ignore it
>                 if [ "${dataset}" != "${pool}" ]; then
>                     # test for encrypted dataset
>                     ENCRYPTIONROOT=$(zfs get -H -o value encryptionroot "${dataset}")
>                     if ! [ "${ENCRYPTIONROOT}" = "-" ]; then
>                         attempt_unlock "${ENCRYPTIONROOT}"
>                     fi
>                 fi
>             done
>         fi
>     fi
> done

Created test datasets via:

# zpool create \
   -o ashift=12 \
   -o autotrim=on \
   -O acltype=posixacl -O xattr=sa -O dnodesize=auto \
   -O compression=zstd \
   -O normalization=formD \
   -O relatime=on \
   -O canmount=off \
   -O recordsize=1M storage-pool2 /dev/vdd
# zfs create storage-pool2/unencrypted
# zfs create -o encryption=on -o keylocation=prompt -o keyformat=passphrase storage-pool2/encrypted
# zfs create storage-pool2/encrypted/backup
# clevis zfs bind -d storage-pool2/encrypted -l boot02 tang '{"url": "http://192.168.1.3/"}'

@aadnehovda aadnehovda mentioned this pull request Sep 14, 2025
@oldium

oldium commented Sep 14, 2025 •

Copy link
Copy Markdown
Contributor

Just curious here why the initramfs-tools integration has not added a script into /etc/zfs/initramfs-tools-load-key.d/ (mentioned here) to load the key via clevis? That would allow using clevis together with zfs-initramfs package on Debian.

The zfs-initramfs package code executed in initramfs is here.

@almereyda

Copy link
Copy Markdown

The recently linked issue has a comment #462 (comment) that reads:

I see some code duplication in the ZFS initramfs hook, it installs basically the same binaries as the regular clevis hook.

Would that observed behaviour of this branch be intentional, misunderstood or an error?

@aadnehovda

Copy link
Copy Markdown

Just curious here why the initramfs-tools integration has not added a script into /etc/zfs/initramfs-tools-load-key.d/ (mentioned here) to load the key via clevis? That would allow using clevis together with zfs-initramfs package on Debian.

The zfs-initramfs package code executed in initramfs is here.

Yep, that is my current workaround:

#!/bin/sh
# /etc/zfs/initramfs-tools-load-key.d/clevis
# Unlock a ZFS encryption root using Clevis-wrapped key material.
#
# How to provision the clevis properties on a dataset:
# Example (TPM+SB):
#   1. Generate a new wrapping for your dataset key:
#      - For passphrase keyformat:
#          echo -n 'your-passphrase' | clevis encrypt tpm2 '{"pcr_bank":"sha256","pcr_ids":"7"}' > passphrase.jwe
#          zfs set clevis:passphrase="$(cat passphrase.jwe)" pool/dataset
#
#      - For hex keyformat:
#          echo -n 'your-hex-key' | clevis encrypt {tpm2,tang,sss} '{/*config*/}' > hex.jwe
#          zfs set clevis:hex="$(cat hex.jwe)" pool/dataset
#
#      - For raw keyformat:
#          cat your-raw.key | clevis encrypt {tpm2,tang,sss} '{/*config*/}' > raw.jwe
#          zfs set clevis:raw="$(cat raw.jwe)" pool/dataset
#
#   2. Verify:
#          zfs get clevis:passphrase pool/dataset
#          zfs get clevis:hex pool/dataset
#          zfs get clevis:raw pool/dataset
#
#   3. Ensure the dataset's keyformat matches the property you set.
#      Example: zfs get keyformat pool/dataset
#
# Environment (from zfs-initramfs):
#   ENCRYPTIONROOT : encryption root dataset
#   ZFS            : helper to run the zfs binary
#
# Contract:
#   Return 0 if we did nothing or successfully unlocked; 1 if we tried and failed.

# Helper: read a property; empty if '-' or error.
get_prop() {
    val="$($ZFS get -H -o value "$1" "$ENCRYPTIONROOT" 2>/dev/null || true)"
    [ "$val" = "-" ] && val=""
    printf '%s' "$val"
}

# passphrase | hex | raw | none
KEYFORMAT="$(get_prop keyformat)"

# Build clevis property name directly from keyformat
CLEVIS_PROP="clevis:$KEYFORMAT"

# Fetch the JWE from that property; log whether found or not.
PROP="$(get_prop "$CLEVIS_PROP")"
if [ -n "$PROP" ]; then
    log_success_msg "ZFS: found $CLEVIS_PROP on $ENCRYPTIONROOT"
else
    log_warning_msg "ZFS: no $CLEVIS_PROP found on $ENCRYPTIONROOT"
    return 0
fi

# Optional: log the Clevis pin type (e.g., tpm2/tang/sss) if jose is present
if command -v jose >/dev/null 2>&1; then
    PIN="$(printf %s "$PROP" \
        | jose jwe fmt -i- \
        | jose fmt -j- -Og protected -yOg clevis -Og pin -Su- 2>/dev/null || true)"
    if [ -n "$PIN" ]; then
        log_success_msg "ZFS: clevis pin=$PIN keyformat=$KEYFORMAT for $ENCRYPTIONROOT"
    fi
fi

# Need clevis to decrypt
if ! command -v clevis >/dev/null 2>&1; then
    log_warning_msg "ZFS: clevis not available in initramfs; skipping"
    return 0
fi

# Unlock using clevis → zfs (zfs interprets bytes according to keyformat)
log_begin_msg "ZFS: unlocking $ENCRYPTIONROOT with clevis ($CLEVIS_PROP)"
printf %s "$PROP" \
  | clevis decrypt \
  | $ZFS load-key -L prompt "$ENCRYPTIONROOT"
ret=$?
log_end_msg $ret
return $ret

@t0rr3sp3dr0

Copy link
Copy Markdown

I'm currently working on adding Clevis support to boot and user pools in TrueNAS Community (a.k.a. Scale) via ZFSBootMenu and would love to see this merged!

@lowjoel

lowjoel commented Dec 1, 2025 •

Copy link
Copy Markdown
Author

I hear all of you -- I'm hoping to work on this when time allows, but Life Happened 😓

@deatharse

Copy link
Copy Markdown

@oldium Thanks I was unaware of / had forgetten about /etc/zfs/initramfs-tools-load-key.d/

@aadnehovda Thanks for adding your script I have taken it as a starting point and reworked it to unlock all datasets not just the root.

@aadnehovda

aadnehovda commented Jan 15, 2026 •

Copy link
Copy Markdown

I have taken it as a starting point and reworked it to unlock all datasets not just the root.

Sounds good. In my case I just used the normal file based keylocation (e.g. /etc/zfs/keys.d/dataset1.key) for those extra datasets. Since the key files are not accessible unless the root filesystem is unlocked first I think it's an okay tradeoff. The datasets will then be decrypted by the zfs-load-key@dataset1 systemd service (autogenerated if configured correctly in /etc/zfs/zfs-list.cache/dataset1).

@lowjoel

lowjoel commented Sep 9, 2026 •

Copy link
Copy Markdown
Author

Sorry this has been languishing for a very long time. It's been working well for me, but since Ubuntu 26.04 has migrated to Dracut I'd do one more push to get both the initramfs-tools and dracut versions working (since I have to migrate my system from Ubuntu 24.04 to 26.04). Trying to summarise all the questions that's been brought up so far:

@deatharse:

your clevis-zfs-bind should implement the -y parameter to "Automatically answer yes for all questions".

I think you're asking me to define the flag, but not actually change any other behaviour since this doesn't really take any inputs or ask questions.

as a nit-pick is the needless use of the function keyword in your bash scripts

Yeah it's inconsistent, I'll remove them all.

I have noticed is if you pass the -f flag "Do not prompt when overwriting configuration" and there is no current config (i.e. the label does not currently exist) the clevis zfs bind command will exit with a status code of 1 and the message:

/usr/libexec/clevis-zfs-common: line 135: new_labels: unbound variable

I think I've added a commit that fixes that.

From further investigation the need for properly configured dropbear to bring up the network is due to clevis-initramfs-zfs_21-1202409290907ubuntu24.04.1_amd64.deb missing those capabilities. The normal clevis-initramfs has the files:

clevis-zfs-initramfs does not supersede clevis-initramfs, both need to be installed. I'll add clevis-initramfs as a dependency of clevis-zfs-initramfs in my next deb build (which I'm rebasing on top of master, as an upcoming v24)

So I've found an issue with unbinding.

I've added another commit that fixes that, good catch.

I've had a bit of a further play by creating a second encrypted pool and noticed that was not unlocked.

I can't decide what the right behaviour here is: on one hand, if I were using initramfs/dracut I really only need the root volume to be unlocked. All other pools can be unlocked subsequently an init.d script or a systemctl unit when the root volume has been unlocked. In my scenario, I unlock the boot volume, and all my encrypted pools used for backups directly call clevis zfs unlock as a systemd unit.

On the other hand, you do have a point. But I imagine adding support for "make this one pool optional but unlock the others" would add substantial complexity. Perhaps we can get the singular pool reviewed and merged first, and figure out the multiple pool case later? As you've added patches for handling multiple pools and different permutations of encryption: there's many things to consider.

@oldium @almereyda @aadnehovda:

Just curious here why the initramfs-tools integration has not added a script into /etc/zfs/initramfs-tools-load-key.d/

It is installed exactly there. See src/initramfs-tools/scripts/zfs-load-key/meson.build -- it just calls clevis zfs unlock. Can you point out where the duplication is? I'll have a look.


I'm trying to rebase my changes onto the latest Clevis, and getting my debs to build again. Hang on for a bit (but you could also apply my latest few commits manually too)

@lowjoel
lowjoel force-pushed the add-zfs-support branch 2 times, most recently from 6fa7185 to 2d2a79c Compare September 10, 2026 07:52
@oldium

oldium commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Usually, it is possible to review the work commit by commit (and commit description giving you the lead - reasoning for the implementation). The Git history of this feature looks like a mess. Just saying, I am not a maintainer of this repo.

. @initramfstoolsdir@/hook-functions

die() {
code="$1"
Comment thread src/zfs/clevis-zfs-list
echo >&2 "The following ZFS datasets have been bound with Clevis:"
# we should not quote ${dataset:-} in case it is empty
# shellcheck disable=SC2086
zfs get -H -o "${output}" -slocal "${zfs_labels_prop}" ${dataset:-}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an external in clevis-zfs-common

Comment thread src/zfs/dracut/clevis-zfs-hook.sh.in Outdated
# load the kernel command line vars
[ -z "$root" ] && root="$(getarg root=)"
# If root is not ZFS= or zfs: or rootfstype is not zfs then we are not supposed to handle it.
[ "${root##zfs:}" = "${root}" ] && [ "${root##ZFS=}" = "${root}" ] && [ "$rootfstype" != "zfs" ] && exit 0

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question: I don't think that we should assume the root commandline arg is set, and we should explicitly call getarg root=. I don't know what rootfstype is though?

@oldium oldium Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rootfstype is also a possible argument accepted by Systemd and also by Dracut:

Moreover, the root and fstype (taken from rootfstype) is globally set in non-Systemd boot here and also in Systemd boot here (saved into /dracut-state.sh reused in other Dracut hooks inside Systemd boot, like pre-mount hook here).

So for you it should be sufficient to check just the root and fstype values?

Comment thread src/zfs/dracut/clevis-zfs-hook.sh.in Outdated
fi

# if pool encryption is active and the zfs command understands '-o encryption'
if [ "$(zpool list -H -o feature@encryption $(echo "${BOOTFS}" | awk -F\/ '{print $1}'))" = 'active' ]; then
Comment thread src/zfs/dracut/clevis-zfs-hook.sh.in Outdated
# if the root dataset has encryption enabled
ENCRYPTIONROOT=$(zfs get -H -o value encryptionroot "${BOOTFS}")
# where the key is stored (in a file or loaded via prompt)
KEYLOCATION=$(zfs get -H -o value keylocation "${ENCRYPTIONROOT}")
mktemp \
jose

inst_hook pre-mount 90 "${moddir}/clevis-zfs-hook.sh"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defined by dracut.

@lowjoel

lowjoel commented Sep 12, 2026

Copy link
Copy Markdown
Author

I've got clevis - 23-1~202609110650~ubuntu24.04.1 ready in my PPAs for jammy, noble, and resolute. I've got Jammy to build after fixing a dependency on dl -- it seems like meson's implicit handling of dl is meson >= 0.62 and jammy ships with 0.61). However, Jammy ships with glibc 2.34 so it's the monolithic libc.so.

I've tested the earlier error cases:

  1. Adding a new label/key when there's no existing labels/keys with -f
  2. A no-op -y has been added to clevis-zfs-bind
  3. Unbinding ZFS keys in chronological order

These now work properly. I've also fixed the following:

  1. clevis-dracut-zfs package wasn't being created
  2. clevis-dracut-zfs' hook was not properly picking up the root=ZFS Linux commandline option (it only included the dataset name, without the zfs: or ZFS= prefix which the hook was looking for)

@oldium:

Usually, it is possible to review the work commit by commit (and commit description giving you the lead - reasoning for the implementation). The Git history of this feature looks like a mess.

I agree with you; I've had to pick up from where @techhazard left off. I can squash them down or even split them up for easier review when 1) this is demonstrated to work 2) there's consensus that this has a path to merge into Clevis. However, going by the fact that #373 has been open since 2022, and this has been open from 2024 with only 3 or 4 people testing this out, I'm unsure if this is a feature that actually is desired to be within Clevis. Therefore, I would hesitate to think that cleaning up for review is the highest priority now. I'd love to be wrong on this, though.

@oldium

oldium commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Usually, it is possible to review the work commit by commit (and commit description giving you the lead - reasoning for the implementation). The Git history of this feature looks like a mess.

I agree with you; I've had to pick up from where @techhazard left off. I can squash them down or even split them up for easier review when 1) this is demonstrated to work 2) there's consensus that this has a path to merge into Clevis. However, going by the fact that #373 has been open since 2022, and this has been open from 2024 with only 3 or 4 people testing this out, I'm unsure if this is a feature that actually is desired to be within Clevis. Therefore, I would hesitate to think that cleaning up for review is the highest priority now. I'd love to be wrong on this, though.

I understand that, but this approach discourages others to have a look at your work and even try it. When I see this mess I ask the same question - can it ever get merged? I personally would answer no and go away. Think positive and keep it in a good shape, be prepared for the best - rewrite the commits, keep it mergeable all the time (I did this with TPM1 pin, kept up to 18 logically structured commits in shape the whole time for almost 2 years, regularly rebasing to master). This will attract the attention by itself.

techhazard and others added 4 commits September 16, 2026 14:27
Co-authored-by: Joel Low <joel@joelsplace.sg>
By storing the list of labels in a well-known location we can use
different keys to unlock the same encryptionroot.

Co-authored-by: Joel Low <joel@joelsplace.sg>
Split the ZFS initramfs hook from the LUKS hook

Installing the ZFS integration should not imply installing the LUKS
integration.

Co-authored-by: Vince van Oosten <techhazard@codeforyouand.me>
@lowjoel

lowjoel commented Sep 16, 2026

Copy link
Copy Markdown
Author

@oldium fair: I've rearranged all the commits logically. Took me a while but here it is.

Co-authored-by: Joel Low <joel@joelsplace.sg>
. "$dracutlib"

# load the kernel command line vars
root="$(getarg root=)"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

root should be already set, same as fstype.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$root was missing the ZFS= at the start that getarg root didn't: the following line would then ignore the dataset. Not sure where the bug is though.

[ "${root##zfs:}" = "${root}" ] && [ "${root##ZFS=}" = "${root}" ] && [ "${fstype}" != "zfs" ] && [ "${fstype}" != "auto" ] && exit 0

# There is a race between the zpool import and the pre-mount hooks, so we wait for a pool to be imported
systemctl start zfs-import.target

@oldium oldium Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is making it Systemd-only and breaks non-Systemd installations. Dracut is also able to work on systems without Systemd. I would personally select a different approach for Systemd - install a simple separate unlocking service and ensure correct startup order by defining/injecting dependencies.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack, but I don't know where else to place this now. This was in the original code @techhazard wrote.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will check it when I have time. I will setup VM with native encrypted ZFS root for testing.

@oldium oldium Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Installing ZFS is really untrivial and when you need to rescue, you need to compile ZFS in live distro... And have Secure Boot disabled, otherwise you will not be able to load the ZFS modules. Well... Why would anyone want that? 😅

Note: You can certainly install the signing certificate to your UEFI BIOS, but new distro - new certificate (unless you keep laying it somewhere near around). Live distro for rescuing - no way to install the certificate (I assume each boot it is different - not verified).

@oldium

oldium commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

I am currently testing solution using ask-password (Dracut/systemd case), zfs wrapper handling zfs load-keys in cases when ask-password is not a universal solution (Dracut/non-systemd case), and initramfs-tools-load-key.d hook (initramfs-tools case).

  • TPM2 works (tested, Debian 12, Dracut/systemd, Dracut/non-systemd, initramfs-tools)
  • TPM1.2 (tested, Debian 12, Dracut/systemd, Dracut/non-systemd, initramfs-tools)

Requirements, drawbacks:

  • Requirement: zfs >= 2.2 (Debian 12 bookworm-backports)
  • Plymouth has fixes ([1], [2]) for Clevis, not yet released. Works, but the password prompt stays until replaced/boot is finished.
  • zfs-dracut has bug in zfs-load-key.sh, which wrongly interprets systemctl binary as Dracut/systemd case, but in fact the binary is added by 99shutdown module for reboot and halt tools, not by 00systemd. Easy to work-around in 90zfs/module-setup.sh for testing purposes (not a permament change)...
  • grub has a bug, so that /etc/grub.d/10_linux does not recognize the pool name of the root filesystem. Easy to fix - use rpool=$(blkid -t TYPE=zfs_member -s LABEL -o value ${GRUB_DEVICE} 2>/dev/null || true) in the xzfs) case branch. Fixed in Grub 2.16 by a different workaround.

TODO: report the bug/fix to zfs-dracut

Update: Tested TPM1.2 as well.

@oldium

oldium commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

So I am done with testing, I will create a separate MR with that work. It went surprisingly smoothly with help of Opus 5.5, it saved me a lot of time actually. Features:

  • Clevis-to-ZFS bind, unbind, the label is optional (defaults to default)
  • Independent on the used Clevis backend, tested with TPM1.2 and TPM2
  • The initramfs-tools variant works
  • The Dracut variant works, both in systemd and non-systemd case
  • Added documentation

And as a bonus:

  • Prepared fix for gnu-grub to detect the metadata correctly
  • Prepared fix for ZFS Dracut to allow running in non-systemd case

The work needs to be pushed, Pull/Merge Requests created. To be done in the afternoon.

@oldium

oldium commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

ZFS packages are ready, testing is done. PR #580 now contains all the work.

@oldium

oldium commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

FYI

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants