└── README.adoc /README.adoc: -------------------------------------------------------------------------------- 1 | Boot-time unlocking of ZFS encrypted datasets 2 | ============================================= 3 | 4 | NOTE: _A disclaimer about the status of this document, as of January 2020:_ 5 | With ZoL version 0.8.1 and newer, systemd mount generators exist that 6 | can automatically take a passphase at boot time and largely obsolete 7 | this guide. If you don’t want the datasets protected by a passphrase 8 | you can type, this document may still serve you well, though the 9 | weakness still lies with the strength of the “keystore” passphrase. 10 | 11 | Background and motivation 12 | ------------------------- 13 | 14 | As of ZFS-on-Linux 0.8.0, native encryption is available and it can be 15 | a desirable feature. On a basic level, it secures data in the case of 16 | computer or disk-drive theft, it ensures that your data won’t be 17 | recovered if the disk as a whole fails and you can’t erase it manually 18 | anymore. It *will not* protect against any kind of attacks while the 19 | system is running. Malware and such will be able to read any 20 | available encrypted dataset freely. This is in common with all other 21 | disk encryption methods: it protects data at-rest, not in-use (where 22 | `zfs get keystatus` is `available`). 23 | 24 | For the full technical details, see 25 | https://www.youtube.com/watch?v=frnLiXclAMo[Tom Caputi’s talk on ZFS 26 | native encryption]. The command set is different from the video (much 27 | earlier in its development), but it largely still holds. 28 | 29 | With all this in mind, I decided to move my home datasets over to 30 | being encrypted. Trusting ZFS’s (current) default with 31 | `encryption=on` (which is the same as `aes-256-ccm`)footnote:[A 32 | default encryption value of `aes-256-ccm` holds true up to 0.8.3, but 33 | the default https://github.com/zfsonlinux/zfs/pull/9749[in a future 34 | release] (whether 0.8.4 or 2.0) is changing to `aes-256-gcm`. It is 35 | recommended to use that now, and to convert ccm datasets to gcm if you 36 | have the time.], I moved my entire `$HOME` dataset tree. Non-raw zfs 37 | sends actually make it pretty easy to convert between encrypted and 38 | unencrypted, and that step is easy… 39 | 40 | Just the one thing: How will I unlock the datasets at boot time, 41 | mostly automatically, in a safe manner? 42 | 43 | There are three possibilities for this: 44 | 45 | 1. A PAM module, with the dataset’s properties as 46 | `keyformat=passphrase` and `keylocation=prompt`. This might be nice, 47 | but no such PAM module exists and I don’t have much interest in 48 | writing it. 49 | 2. With `keyformat=passphrase` and `keylocation=prompt`, a special 50 | boot service that prompts for the passphrase. This already exists for 51 | cryptsetup, the implementation of which seems to be built into systemd 52 | and possibly could be adapted for `zfs load-key`. I made attempts to 53 | write one using `systemd-ask-passphrase`, but was unsuccessful, which 54 | leads to the last and actual implementation I ended up on… 55 | 3. `keyformat=raw` and `keylocation=file://$PATH`, ZFS can 56 | automatically load the key from a 32-byte file of random data. This 57 | needs to be preloaded and requires a secure location for this file 58 | itself (if the key is available unencrypted, all the time, your data 59 | may as well be unencrypted too). On the plus side, as I mentioned in 60 | the last point, cryptsetup already has perfectly good support for 61 | boot-time unlocking and may be used to assist with this problem. 62 | 63 | The solution I use for myself is the third one. The key file might be 64 | stored on a USB drive for temporary access when required, but I don’t 65 | have a desire to dedicate a USB drive for such a purpose. I’d much 66 | rather have the system be pretty self-suficient, which means I’ll need 67 | a zvol encrypted with LUKS (_not ZFS’s encryption_), which gets 68 | unlocked with a passphrase, mounted, and `zfs load-key` works. 69 | 70 | Step 1: Make the zvol 71 | ~~~~~~~~~~~~~~~~~~~~~ 72 | 73 | LUKS actually has a surprisingly large header, I found, when my first 74 | attempt to make a 10M zvol failed as soon as I tried to luksFormat it. 75 | A larger zvol should not be a very big deal (I have a 4TB pool in my 76 | desktop), and I opted to create a 50M volume, then format it with 77 | cryptsetup. For good measure, I fill up all the available space 78 | first, then do a mkfs: 79 | 80 | ---- 81 | # zfs create -V 50m rpool/keystore 82 | # cryptsetup luksFormat /dev/zvol/rpool/keystore 83 | (type YES and your passphrase) 84 | # cryptsetup open --type luks /dev/zvol/rpool/keystore keystore 85 | # cp /dev/zero /dev/mapper/keystore 86 | # mkfs.fat /dev/mapper/keystore 87 | ---- 88 | 89 | I choose FAT because it is a very simple file system, and with the 90 | right mount options, you never need to worry about a program 91 | accidentally creating insecure permissions. This part can easily be 92 | adjusted to have an ext2 formatted volume or anything, if desired. 93 | 94 | Add the LUKS volume to `/etc/crypttab`, which should trigger systemd 95 | at boot time to prompt for the passphrase: 96 | 97 | ---- 98 | # echo 'keystore /dev/zvol/rpool/keystore none' >> /etc/crypttab 99 | ---- 100 | 101 | The third field is “password” and the value of “none” causes systemd 102 | to prompt for the password interactively. This was not intuitive to 103 | me, but it is what it is. 104 | 105 | Add the volume to `/etc/fstab` and mount it. Just for my own peace of 106 | mind, I keep it mounted read-only by default so no possibility of 107 | writes or corruption happens, so the mount command needs an extra 108 | option. 109 | 110 | ---- 111 | # mkdir /etc/keystore 112 | # echo '/dev/mapper/keystore /etc/keystore vfat dmask=077,fmask=177,ro 0 2' >> /etc/fstab 113 | # mount -o rw /etc/keystore 114 | ---- 115 | 116 | Step 2: Generate keys 117 | ~~~~~~~~~~~~~~~~~~~~~ 118 | 119 | This is pretty straight-forward, ZFS only needs files 32 bytes of 120 | length of random data for its key (when `keyformat=raw`). This may be 121 | accomplished as simply as this: 122 | 123 | ---- 124 | # dd if=/dev/urandom of=/etc/keystore/home-user.key bs=32 count=1 125 | ---- 126 | 127 | The file name is arbitrary and may be whatever you like. I generally 128 | like the form of `${DATASET}.key`. 129 | 130 | If you have locate(1) on your system, updatedb(8) can leak the entire 131 | directory tree of your datasets. I feel this is undesirable, and I 132 | opt to make an encrypted dataset for its database so that it may still 133 | operate fully normally, but my file names are still safe from prying 134 | eyes. An alternative is to exclude paths in `/etc/updatedb.conf`, but 135 | then the usefulness of the tool is gone for me. 136 | 137 | ---- 138 | # dd if=/dev/urandom of=/etc/keystore/var-lib-mlocate.key bs=32 count=1 139 | ---- 140 | 141 | Add as many files as needed. There is no technical requirement that 142 | each encryptionroot needs to have a unique key, but it is a very good 143 | idea nonetheless. 144 | 145 | Step 3: Set appropriate dataset properties 146 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 147 | 148 | This could be done at `zfs create` time, or `zfs receive`, if planning 149 | ahead, but I didn’t. Luckily, the actual key ZFS uses to encrypt 150 | datasets is not really user-visible and the file and/or passphrase is 151 | merely a way to unlock this hidden key. This makes it pretty easy to 152 | modify the values after the fact, to change passphrase or file. (LUKS 153 | actually works in the exact same way.) 154 | 155 | I’ll still document all the methods: 156 | 157 | From scratch: 158 | ---- 159 | # zfs create -o encryption=aes-256-gcm -o keyformat=raw -o keylocation=file:///etc/keystore/home-user.key rpool/home/user 160 | ---- 161 | 162 | From a send stream (non-raw only, read the manpage!), adjust redirect 163 | or pipe accordingly: 164 | ---- 165 | # zfs receive -o encryption=aes-256-gcm -o keyformat=raw -o keylocation=file:///etc/keystore/home-user.key rpool/home/user < /some/place/zfs-sendstream 166 | ---- 167 | 168 | After the fact, if you created it first with a passphrase and change 169 | to this method: 170 | ---- 171 | # zfs change-key -o keyformat=raw -o keylocation=file:///etc/keystore/home-user.key rpool/home/user 172 | ---- 173 | 174 | I also created a `rpool/var/lib/mlocate` dataset to protect against 175 | the file tree from being leaked. This location might have to be 176 | adjusted per locate(1) implementation. 177 | 178 | Step 4: systemd service to run `zfs load-key` 179 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 180 | 181 | After all of this, we are almost done. Just need one more piece of 182 | the puzzle, so that zfs can load the keys for datasets and 183 | automatically mount them afterward. 184 | 185 | ---- 186 | # cat > /etc/systemd/system/zfs-load-key.service <