Early boot remote decryption on NixOS

To remotely reboot a system with an encrypted root file system, one can provide the encryption keys via SSH during early boot. To that end, networking utilities and an SSH server need to be installed in the initial ramdisk.

Note that the SSH host keys will need to be stored in plain text, so an attacker with physical access can extract them and perform a MITM attack to intercept the keys when you try to reboot the system. Hence compared to an unencrypted disk this approach warrants only limited additional trust; if security matters, go on premise to reboot and verify the integrity of your hardware, use a TPM to tie the storage encryption keys to your system, or enforce physical security. As an added disclaimer, I am not a security researcher.

After some trial and error, I got a working configuration with the following options. First, to configure networking:

{ lib, ... }: {
  boot.initrd.network.enable = true;
  # Network card drivers. Check `lshw` if unsure.
  boot.initrd.kernelModules = [ "smsc95xx" "usbnet" ];
  # It may be necessary to wait a bit for devices to be initialized.
  # See https://github.com/NixOS/nixpkgs/issues/98741
  boot.initrd.preLVMCommands = lib.mkBefore 400 "sleep 1";
  # Your post-boot network configuration is taken
  # into account. It should contain:
  networking.useDHCP = false;
  networking.interfaces.<interface>.useDHCP = true;
}

Then, to install cryptsetup and necessary device drivers:

{
  boot.initrd.luks.forceLuksSupportInInitrd = true;
  # If necessary, kernel modules required to access
  # the root device. For example, on a Raspberry Pi
  # 3B+, with the root disk attached via USB, this is:
  boot.initrd.availableKernelModules = [ "usb_storage" ];
}

And finally, to set up an OpenSSH server with a shell prompt asking for encryption keys:

{ config, ... }: {
  boot.initrd = {
    network.ssh = {
      enable = true;
      # Defaults to 22.
      port = 222;
      # Stored in plain text on boot partition, so don't reuse your host
      # keys. Also, make sure to use a boot loader with support for initrd
      # secrets (e.g. systemd-boot), or this will be exposed in the nix store
      # to unprivileged users.
      hostKeys = [ "/etc/ssh/initrd_ssh_host_ed25519_key" ];
      # I'll just authorize all keys authorized post-boot.
      authorizedKeys = config.users.users.root.openssh.authorizedKeys.keys;
    };
    # Set the shell profile to meet SSH connections with a decryption
    # prompt that writes to /tmp/continue if successful.
    network.postCommands = let
      # I use a LUKS 2 label. Replace this with your disk device's path.
      disk = "/dev/disk/by-label/crypt";
    in ''
      echo 'cryptsetup open ${disk} root --type luks && echo > /tmp/continue' >> /root/.profile
      echo 'starting sshd...'
    '';
    # Block the boot process until /tmp/continue is written to
    postDeviceCommands = ''
      echo 'waiting for root device to be opened...'
      mkfifo /tmp/continue
      cat /tmp/continue
    '';
  };
}

For the record, here's my bootloader configuration:

{
  boot.loader = {
    systemd-boot = {
      enable = true;
      configurationLimit = 10;
      editor = false;
    };
    efi.canTouchEfiVariables = false;
  };
}