[Solved] Full disk encryption unlock automatically via USB

Hi, I’m new to NixOS and I was trying to setup a full disk encryption to unlock via boot when the key is on a different partition over a USB. However I didn’t quite get how to achieve that, during the first boot up it says that can’t find the partition with the key and can’t create the mount point /usbstick.

These are my partitions:

NAME        FSTYPE      FSVER            LABEL                      UUID                                 FSAVAIL FSUSE% MOUNTPOINTS
loop0       squashfs    4.0                                                                                    0   100% /nix/.ro-store
sda         iso9660     Joliet Extension nixos-minimal-24.05-x86_64 1980-01-01-00-00-00-00                              
├─sda1      iso9660     Joliet Extension nixos-minimal-24.05-x86_64 1980-01-01-00-00-00-00                     0   100% /iso
└─sda2      vfat        FAT12            EFIBOOT                    1234-5678                                           
sdb                                                                                                                     
├─sdb1      vfat        FAT32            boot                       9413-748A                              1022M     0% /mnt/boot
└─sdb2      vfat        FAT32                                       27XE-1D71                                           /mnt/usbstick
nvme0n1                                                                                                                 
└─nvme0n1p1 crypto_LUKS 2                                           cf803e7d-53rf-400c-560a-8ff7b24ba48f                
  └─crypted ext4        1.0              nixos                      5aacf2ef-18fd-4fdc-b139-76322c2e46b1    1.7T     0% /mnt

Where the key to unlock my luks partition is in /mnt/usbstick.

During the partitioning I have used the following command:

cryptsetup -y -v luksFormat /dev/nvme0n1p1 /mnt/usbstick/key
cryptsetup open /dev/nvme0n1p1 crypted --key-file /mnt/usbstick/key

After the partitioning is done, I have generate the configuration file.
I’m not sure everything is in place here:

{ config, lib, pkgs, ... }:

let
  PRIMARYUSBID = "27XE-1D71";
in 
{
  imports =
    [ # Include the results of the hardware scan.
      ./hardware-configuration.nix
    ];

   # Kernel modules needed for mounting USB VFAT devices in initrd stage
   boot.initrd.kernelModules = [ "usb_storage" "vfat" "nls_cp437" ];

    # Mount USB key before trying to decrypt root filesystem
  boot.initrd.postDeviceCommands = pkgs.lib.mkBefore ''
    mkdir -m 0755 -p /usbstick
    sleep 2 # To make sure the usb key has been loaded
    mount -n -t vfat -o ro `findfs UUID=${PRIMARYUSBID}` /usbstick
  '';

  boot.initrd.luks.devices."crypted" = {
    keyFile = "/usbstick/keyfile";
    # I don't have LVM partitions, so I'm sure this is necessary
    preLVM = false; # If this is true the decryption is attempted before the postDeviceCommands can run
  };
}
<SNIP>

In another distribution I have the same setup but I’m using systemd-boot to let it know where the key is with:

options cyrptdevice=UUID=<luks_partion_UUID>:cryptroot root=/dev/mapper/cryptroot cryptkey=UUID=<USB_UUID>:vfat:/<key_name> rw

As an alternative in case I make the first solution work, would be possible to achieve the same in NixOS?

Those cyrptdevice= and cryptkey= parameters are features of your initramfs on that other distro. NixOS has similar stuff but it’s different.

This seems like it would benefit quite a bit from the systemd-based initrd (note, this is a different thing than systemd-boot). The slightly odd little quirk would be getting the file system mounted, though you’ve given me an idea (more on that later).

boot.initrd.luks.devices.crypted.keyFile = "/usbstick/keyfile";
boot.initrd.systemd = {
  enable = true;
  contents."/etc/fstab".text = ''
    UUID=${PRIMARYUSBID} /usbstick vfat ro
  '';
}

This should actually be all you need. preLVM has no effect in systemd initrd, because systemd handles dependencies dynamically. And postDeviceCommands doesn’t exist with systemd initrd, because systemd initrd uses declarative systemd units instead of imperative shell script fragments. With this, systemd should basically figure everything out for you.

Explanation: When a key file is specified, systemd automatically creates a dependency on having all the file systems in the path to the key file being mounted. The /etc/fstab trick I did there then adds an fstab to initrd, which it normally doesn’t have (don’t worry, there’s a different fstab for your root file system that it uses), and this fstab file causes systemd to understand how to mount /usbstick during stage 1. Systemd automatically handles all the dependencies and ordering, and everything should just work. You should even be able to just plug in the USB stick whenever you want and systemd will just wait for it to appear.

Now, about that idea I said I had… It makes me want a new option, like boot.initrd.fileSystems, that generates the in-initrd /etc/fstab file like the one I just put up there. Then it could have been as simple as:

boot.initrd.systemd.enable = true;
boot.initrd.luks.devices.crypted.keyFile = "/usbstick/keyfile";
boot.initrd.fileSystems."/usbstick" = {
  device = "UUID=${PRIMARYUSBID}";
  fsType = "vfat";
  options = ["ro"];
};
1 Like

Thanks for the help!
I’m still having some issue but at least I’m able to get ahead during the first booting steps.
It seems now it’s upset about the font used (maybe) did some quick googling.

<SNIP>
[FAILED] Failed to start Virtual Console Setup.
<SNIP>

I’ll try to change to another font rather than the one used by default in the config file, if I get to succeed I’ll mark the post as solved.

Agree, this looks better:

boot.initrd.systemd.enable = true;
boot.initrd.luks.devices.crypted.keyFile = "/usbstick/keyfile";
boot.initrd.fileSystems."/usbstick" = {
  device = "UUID=${PRIMARYUSBID}";
  fsType = "vfat";
  options = ["ro"];
};

That’s interesting, and seems like maybe a bug? Can you share more of your config?

Tangential but relevant: FOSDEM 2024 - Clevis/Tang: unattended boot of an encrypted NixOS system

Almost the whole config file.
I have removed some of the entries listed not really relevant to shorten everything, where <SNIP> is used everything is left commented.

dit this configuration file to define what should be installed on
# your system. Help is available in the configuration.nix(5) man page, on
# https://search.nixos.org/options and in the NixOS manual (`nixos-help`).

{ config, lib, pkgs, ... }:
let
  PRIMARYUSBID = "27XE-1D71";
in 
{
  imports =
    [ # Include the results of the hardware scan.
      ./hardware-configuration.nix
    ];

  # Kernel modules needed for mounting USB VFAT devices in initrd stage
  boot.initrd.kernelModules = [ "uas" "usbcore" "usb_storage" "vfat" "nls_cp437" "nls_iso8859_1" ];
  boot.initrd.luks.devices.crypted.keyFile = "/usbstick/keyfile";
  boot.initrd.systemd = {
     enable = true;
     contents."/etc/fstab".text = ''
        UUID=${PRIMARYUSBID} /usbstick vfat ro
     '';
  };

  # Use the systemd-boot EFI boot loader.
  boot.loader.systemd-boot.enable = true;
  boot.loader.efi.canTouchEfiVariables = true;

  # networking.hostName = "nixos"; # Define your hostname.
  # Pick only one of the below networking options.
  # networking.wireless.enable = true;  # Enables wireless support via wpa_supplicant.
  networking.networkmanager.enable = true;  # Easiest to use and most distros use this by default.

  # Set your time zone.
  <SNIP>

  # Configure network proxy if necessary
  <SNIP>

  # Select internationalisation properties.
  i18n.defaultLocale = "en_US.UTF-8";
  console = {
     font = "Lat2-Terminus16";
  #   keyMap = "us";
     useXkbConfig = true; # use xkb.options in tty.
  };

  # Enable the X11 windowing system.
  # services.xserver.enable = true;


  

  # Configure keymap in X11
  # services.xserver.xkb.layout = "us";
  # services.xserver.xkb.options = "eurosign:e,caps:escape";

  # Enable CUPS to print documents.
  # services.printing.enable = true;

  # Enable sound.
  <SNIP>

  # Enable touchpad support (enabled default in most desktopManager).
  # services.libinput.enable = true;

  # Define a user account. Don't forget to set a password with ‘passwd’.
  <SNIP>

  # List packages installed in system profile. To search, run:
  # $ nix search wget
   environment.systemPackages = with pkgs; [
     vim # Do not forget to add an editor to edit configuration.nix! The Nano editor is also installed by default.
  #   wget
  ];

<SNIP>

}

EDIT:
I have left this commented:

 # Select internationalisation properties.
 #  i18n.defaultLocale = "en_US.UTF-8";
 #  console = {
 #     font = "Lat2-Terminus16";
 #     keyMap = "us";
 #     useXkbConfig = true; # use xkb.options in tty.
 # };

It seems to be enough to not get the [FAILED] Failed to start Virtual Console Setup. error.
Kudos to you @ElvishJerricco! Everything working fine now.

I hope to see in the future that new option proposed by you.
Marking this post a solved.