ZFS Failed to open key material file

When using a prompted passphrase for the root dataset and an agenix path for the home dataset, I’m getting an failed to open key material file error. I’m assuming this is because ZFS is recursively loading the keys and the agenix secret isn’t available yet? If so, is there a way to load the home dataset after agenix, but the root dataset before? This way agenix would still have access to the persisted identities and can decrypt the secret necessary for the home dataset.

You probably just need to set boot.zfs.requestEncryptionCredentials = [ "the/root/data/set" ]; and add a systemd service that does zfs load-key the/home/data/set in stage 2.

1 Like

Hmm… is there a way to run a command before unlocking the pool?

Well, importing and unlocking happen in the same systemd unit, and that unit doesn’t have any kind of hooks, so no. Even if we did want to make that more granular, it would probably just be by splitting it into more systemd units, so running a command in the middle would still end up just meaning adding a systemd service for your command. What do you imagine it doing?

1 Like

I was initially using an agenix path for the root key location, but now I’m thinking I could keep an age file in the boot partition and decrypt it with my yubikey to the /run directory right before the pool is imported. Would that work?

but… why? That seems way more complicated and difficult (the boot partition isn’t mounted in stage 1, for instance). You could just defer the home dataset decryption until after agenix has happened.

oh wait, you said you’re using agenix for the root dataset? Huh? How have you had that working?

I haven’t, that’s the problem. Is the run directory available during stage 1 before the zpool? If so, I can readFile the encrypted secret and decrypt it to the expected location right before import.

Yes, /run exists in stage 1, though you generally shouldn’t just put keys in there since that tmpfs is allowed to swap to disk. You probably wouldn’t use readFile, since you can just use string interpolation to copy the file into the initrd like boot.initrd.systemd.storePaths = [ "${./encrypted-file}" ];.

But what exactly are you trying to accomplish at this point? If you just want to enter a passphrase for the root dataset and have the home dataset automatically decrypted, absolutely none of this is necessary and you can just have a passphrase on the root dataset and have the home dataset inherit encryption from it. If the goal is automatic decryption via yubikey, that’s more complicated (though I have ideas). But the main questions are what do you actually want the security model to be and what do you want the UX to be?

Basically, I want to be able to boot in as long as I have the yubikey, preferably without a pin and without touching the contact. And regarding secrets in /run, would that mean agenix and sops-nix aren’t very secure…? Should I change the key paths to another directory?

IIUC those will use a different mountpoint that they add under /run. Like a separate ramfs (which doesn’t get swapped) or a separate tmpfs with noswap.

It’s going to be simpler to just ignore agenix for this then. Agenix is all about the stuff that happens after this sort of stuff is already done. Honestly the simplest thing might just be to add something like age (the tool itself, not agenix) and its yubikey plugin, and make a systemd service ordered before zfs-import-${poolName}.service that uses age / the yubikey to decrypt an encrypted file added to the initrd via boot.initrd.systemd.storePaths or boot.initrd.systemd.contents to a location that the pool expects to find it at (which would need to also be set up as one of those places that won’t swap).

Another option, which is similar to stuff I actually do on my systems, is to just have a very small (like a few MiB) zvol device on top of the pool that does not use ZFS encryption and instead is encrypted with LUKS so it can be used with normal systemd file system and decryption tools like systemd-cryptenroll (which supports FIDO2, which most yubikeys support). Storing the ZFS key file on a file system on LUKS on zvol like this is exactly what I do, though with the TPM2, and admittedly it’s kinda complicated. But I do it that way because it integrates nicely with systemd and also lets me have things like a recovery passphrase LUKS slot to decrypt when the hardware key isn’t available.

2 Likes

Hmm… Is it possible to implement your setup in disko, if you don’t use it already? Or would using an age-plugin-tpm identity with your first suggestion work as well, in case the yubikeys aren’t available?

Would something like this work for the first suggestion? How can I ensure it’s set up before the zpool is imported?

fileSystems."/boot.d" = {
    device = "tmpfs";
    fsType = "tmpfs";
    options = [ "defaults" "size=4M" "nosuid" "nodev" "noswap" ];
};

Well fileSystems fundamentally depend on the root FS already being mounted, so you can’t use them for anything that comes before the root FS is mounted. Something along these lines might work?

boot.initrd.systemd = {
  contents."/my-encrypted-key".source = ./my-encrypted-key;
  mounts = [
    {
      what = "none";
      type = "tmpfs";
      # /run/keys already belongs to nixos, but doesn't get mounted until too late
      where = "/run/mykeys";
      wantedBy = [ "initrd.target" ];
      options = "defaults,noswap";
    }
  ];
  services."decrypt-my-key" = {
    wantedBy = [ "initrd.target" ];
    unitConfig = {
      RequiresMountsFor = "/run/mykeys";
      DefaultDependencies = false;
    };
    before = [ "zfs-import-${poolName}.service" ];
    path = [
      pkgs.age
      pkgs.age-plugin-yubikey
    ];
    script = ''
      # I don't know what code you'd use, but the goal
      # here is to decrypt the file that's in the
      # initrd at /my-encrypted-key and store it
      # somewhere in /run/mykeys/
    '';
  };
};

And then you’d just have your ZFS dataset’s keylocation property set to file:///run/mykeys/output-file

1 Like

I just realized: do I not need to set up the pcscd daemon? It’s required for agenix to use yubikeys during the activation scripts, for example.

uhh maybe I’m not familiar enough with PIV, because I don’t know if that would be necessary or not. I had assumed that since this was an age plugin specifically for yubikeys, not a PKCS#11 application, that something like pcscd wouldn’t be needed. But if so, then maybe it’s worth considering GitHub - olastor/age-plugin-fido2-hmac: Age plugin to encrypt files with fido2 tokens using the hmac-secret extension and non-discoverable credentials. · GitHub instead? (FIDO2 hmac-secret is the same thing that systemd-cryptenroll’s FIDO2 support is based on)

Hmm… From their README:

FIDO2 does not provide an API for asymmetric encryption; instead, this plugin uses the hmac-secret extension to protect an Age identity with your hardware key. For decryption, the identity is transferred to your computer’s memory. If this identity is stolen from memory, it can be used without the token to decrypt past and future data meant for this identity.

Isn’t this technically what’s happening when secrets are decrypted to a RAM-based filesystem? If so, should I worry about this scenario?

That issue is unavoidable with disk encryption, unless you want every single byte that gets read or written to disk to travel through your yubikey on its way out or in. Both LUKS and ZFS encryption require the real effective key to be in kernel memory for the kernel to do encryption with itself. Decrypting that key from metadata on disk is what you’re doing when you do cryptsetup open or zfs load-key.

1 Like

Got it. I’ll look into it. However, in the meantime, could you give me your thoughts on this discussion? It seems to imply that the yubikey would work during stage 1 if the udev is available, which is how LUKS seems to get them to work.

It seems to be the case:

Jul 17 02:22:59 iso systemd[1]: Started decrypt-iso-password.service.
Jul 17 02:22:59 iso decrypt-iso-password-start[348]: loading "/nix/store/qpmypc2hndyyxk30zv0xi0qcfx739f5l-pcsclite-2.4.1-lib/lib/libpcsclite_real.so.1" failed: /nix/store/qpmypc2hndyyxk30zv0xi0qcfx739f5l-pcsclite-2.4.1-lib/lib/libpcsclite_real.so.1: cannot open shared object file: No such file or directory
Jul 17 02:22:59 iso decrypt-iso-password-start[348]: Error: Error while communicating with YubiKey: PC/SC error: An internal consistency check failed
Jul 17 02:22:59 iso decrypt-iso-password-start[348]: Cause: An internal consistency check failed
Jul 17 02:22:59 iso decrypt-iso-password-start[348]: [ Did this not do what you expected? Could an error be more useful? ]
Jul 17 02:22:59 iso decrypt-iso-password-start[348]: [ Tell us: https://str4d.xyz/age-plugin-yubikey/report              ]
Jul 17 02:22:59 iso systemd[1]: decrypt-iso-password.service: Main process exited, code=exited, status=1/FAILURE
Jul 17 02:22:59 iso systemd[1]: decrypt-iso-password.service: Failed with result 'exit-code'.

Which means it cannot access the pcscd daemon. Is there any way to implement the following fix in stage 1, adapted from here?

echo "[setupYubikeys] Adding age plugins to path..."
PATH=$PATH:${lib.makeBinPath config.age.plugins.packages}

echo "[setupYubikeys] Making pcsc directory and linking drivers..."
${pkgs.runtimeShell} -c "mkdir -p /var/lib/pcsc && ln -sfn ${pkgs.ccid}/pcsc/drivers /var/lib/pcsc/drivers"

# TODO: https://github.com/Mic92/sops-nix/issues/377#issuecomment-3263198635
echo "[setupYubikeys] Killing pcscd..."
pgrep pcscd > /dev/null && pkill pcscd

echo "[setupYubikeys] Running pcscd..."
pcscd

echo "[setupYubikeys] Pcscd running."

For example, /var/lib would not be accessible, correct? Could I create a mount for it in ram instead?

1 Like

Also, would you happen to know if I could use this commit somehow to create a fallback to prompt the user for the passphrase in case the yubikeys or tpm key aren’t available?