From 239fbf70e0e157f233fb63feb9c9c6aa134e6937 Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Fri, 4 Apr 2025 16:43:44 +0200 Subject: [PATCH] Use file to unlock LUKS from usb media --- hosts/iron/configuration.nix | 57 +++++++++-------- justfile | 10 +-- modules/default.nix | 1 + modules/luksusb.nix | 116 +++++++++++++++++++++++++++++++++++ 4 files changed, 154 insertions(+), 30 deletions(-) create mode 100644 modules/luksusb.nix diff --git a/hosts/iron/configuration.nix b/hosts/iron/configuration.nix index e52a1d1..f130428 100644 --- a/hosts/iron/configuration.nix +++ b/hosts/iron/configuration.nix @@ -1,13 +1,13 @@ { inputs, config, pkgs, lib, ... }: let interfaces = import ./interfaces.nix; - disks = [ - "ata-Samsung_SSD_870_QVO_8TB_S5SSNG0R103837K" - "ata-Samsung_SSD_870_QVO_8TB_S5SSNG0R103838A" - "ata-Samsung_SSD_870_QVO_8TB_S5SSNG0R104926N" - "ata-Samsung_SSD_870_QVO_8TB_S5SSNG0R104934H" - "ata-Samsung_SSD_870_QVO_8TB_S5SSNJ0W206517Y" - ]; + disks = { + slot1 = "ata-Samsung_SSD_870_QVO_8TB_S5SSNG0R103837K"; + slot2 = "ata-Samsung_SSD_870_QVO_8TB_S5SSNG0R103838A"; + slot3 = "ata-Samsung_SSD_870_QVO_8TB_S5SSNG0R104926N"; + slot4 = "ata-Samsung_SSD_870_QVO_8TB_S5SSNG0R104934H"; + slot5 = "ata-Samsung_SSD_870_QVO_8TB_S5SSNJ0W206517Y"; + }; removableEfi = true; devNodes = "/dev/disk/by-id/"; datasets = { @@ -28,7 +28,7 @@ let luksDev = "-part3"; biosBoot = "-part4"; }; - efiSystemPartitions = (map (diskName: diskName + partitionScheme.efiBoot) disks); + efiSystemPartitions = (map (diskName: diskName + partitionScheme.efiBoot) (lib.attrValues disks)); in with lib; { imports = [ @@ -116,6 +116,19 @@ with lib; { ia_pd 1/::/64 ${interfaces.lan}/0/64 ''; + jalr.luksUsbUnlock = { + enable = true; + devices = builtins.mapAttrs + (name: dev: + { + keyPath = "iron.key"; + usbDevice = "by-label/RAM_USB"; + waitForDevice = 10; + } + ) + disks; + }; + boot = { kernel.sysctl = { "net.ipv6.conf.all.forwarding" = 1; @@ -130,20 +143,14 @@ with lib; { "xhci_pci" ]; systemd.enable = true; - luks.devices = lib.listToAttrs ( - map - (dev: { - name = "LUKS-${dev}${partitionScheme.luksDev}"; - value = { - device = "${devNodes}${dev}${partitionScheme.luksDev}"; - allowDiscards = true; - keyFileSize = 4096; - keyFile = "/dev/disk/by-id/usb-jalr_USB_RAM_disk_prototype-01-0:0"; - keyFileTimeout = 60; - }; - }) - disks - ); + luks.devices = builtins.mapAttrs + (name: dev: + { + device = "${devNodes}${dev}${partitionScheme.luksDev}"; + allowDiscards = true; + } + ) + disks; }; supportedFilesystems = [ "zfs" ]; zfs = { @@ -153,13 +160,13 @@ with lib; { loader = { efi = { canTouchEfiVariables = (if removableEfi then false else true); - efiSysMountPoint = ("/boot/efis/" + (head disks) + efiSysMountPoint = ("/boot/efis/" + (head (lib.attrValues disks)) + partitionScheme.efiBoot); }; generationsDir.copyKernels = true; grub = { enable = true; - devices = (map (diskName: devNodes + diskName) disks); + devices = (map (diskName: devNodes + diskName) (attrValues disks)); efiInstallAsRemovable = removableEfi; copyKernels = true; efiSupport = true; @@ -173,7 +180,7 @@ with lib; { (diskName: '' ${pkgs.coreutils-full}/bin/cp -r ${config.boot.loader.efi.efiSysMountPoint}/EFI /boot/efis/${diskName}${partitionScheme.efiBoot} '') - (tail disks))); + (tail (attrValues disks)))); }; }; kernelParams = [ diff --git a/justfile b/justfile index 6186025..d1d2a9a 100644 --- a/justfile +++ b/justfile @@ -1,4 +1,5 @@ -usb_ram_disk := "/dev/disk/by-id/usb-jalr_USB_RAM_disk_prototype-01-0:0" +usb_ram_disk := "/dev/disk/by-label/RAM_USB" +usb_ram_mountpoint := shell("findmnt -n -o TARGET $1 || true", usb_ram_disk) boot: nixos-rebuild boot --flake . --use-remote-sudo @@ -26,9 +27,8 @@ repl: " luks-pass host: - @if [ -b "{{usb_ram_disk}}" ]; then \ - gpg -d hosts/{{host}}/luks-passfile.gpg | sudo dd of={{usb_ram_disk}}; \ + @if [ -d "{{usb_ram_mountpoint}}" ]; then \ + gpg -d hosts/{{host}}/luks-passfile.gpg > "{{usb_ram_mountpoint}}/{{host}}.key"; \ else \ - echo "{{usb_ram_disk}} is not a block device" >&2; \ + echo "Mount point not found. Is the usb device plugged and mounted?" >&2; \ fi - diff --git a/modules/default.nix b/modules/default.nix index 908dfd9..ff6f458 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -32,6 +32,7 @@ ./kvm-switch-enable-screen.nix ./libvirt.nix ./localization.nix + ./luksusb.nix ./mailserver ./matrix ./mobile-network.nix diff --git a/modules/luksusb.nix b/modules/luksusb.nix new file mode 100644 index 0000000..1edb442 --- /dev/null +++ b/modules/luksusb.nix @@ -0,0 +1,116 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.jalr.luksUsbUnlock; +in +{ + options.jalr.luksUsbUnlock = with lib; with lib.types; { + enable = mkEnableOption "unlock LUKS volumes with a USB device on boot"; + devices = mkOption { + default = { }; + example = { + cryptroot = { + keyPath = "/path/to/the/key"; + usbDevice = "by-label/MY_USB"; + }; + }; + type = types.attrsOf (types.submodule { + options = { + keyPath = mkOption { + example = "/mykey.key"; + description = mdDoc '' + Path to the key file inside the USB device's filesystem. + `/` is relative to the device's filesystem root. + ''; + type = types.str; + }; + + usbDevice = mkOption { + example = "by-label/BOOTKEY"; + description = mdDoc '' + Path to the USB device that contains the keys. (Path relative to `/dev/disk/`) + ''; + type = types.str; + }; + + waitForDevice = mkOption { + default = 5; + example = 10; + description = mdDoc '' + How many seconds to wait for the USB device to be detected by the + kernel. + ''; + type = types.ints.unsigned; + }; + }; + }); + }; + }; + config = lib.mkIf cfg.enable + ( + let + makeUsbDevPath = usbDevice: "/dev/disk/" + usbDevice; + makeMountPath = usbDevice: "/key/" + (builtins.hashString "md5" usbDevice); + usbFsType = "vfat"; + + mapAttrsNameValue = f: set: + lib.listToAttrs (map f (lib.attrsToList set)); + in + { + boot.initrd.kernelModules = [ "uas" "usbcore" "usb_storage" "vfat" "nls_cp437" "nls_iso8859_1" ]; + + boot.initrd.systemd.services = + let + makeService = name: { keyPath, usbDevice, waitForDevice }: + let + usbDevPath = makeUsbDevPath usbDevice; + usbMountPath = makeMountPath usbDevice; + in + { + description = "Mount ${name} key"; + wantedBy = [ "cryptsetup.target" ]; + before = [ "systemd-cryptsetup@${name}.service" ]; + after = [ "systemd-modules-load.service" ]; + unitConfig.DefaultDependencies = "no"; + serviceConfig.Type = "oneshot"; + + script = '' + if awk -v mountpoint="${usbMountPath}" '$2==mountpoint {f=1} END {exit !f}' /proc/mounts; then + exit 0 + fi + + attempts=0 + while [ ! -e ${lib.escapeShellArg usbDevPath} ]; do + sleep 1 + if [ $attempts -ge ${toString waitForDevice} ]; then + break; + fi + attempts=$((attempts+1)) + done + + if [ -e ${lib.escapeShellArg usbDevPath} ]; then + mkdir -m0500 -p ${lib.escapeShellArg usbMountPath} + mount -n -t ${lib.escapeShellArg usbFsType} -o ro,fmask=0137,dmask=0027 ${lib.escapeShellArg usbDevPath} ${lib.escapeShellArg usbMountPath} + fi + ''; + }; + in + mapAttrsNameValue + ({ name, value }: { + name = "luksusb-${name}"; + value = makeService name value; + }) + cfg.devices; + + boot.initrd.luks.devices = builtins.mapAttrs + (name: { keyPath, usbDevice, ... }: + let + usbMountPath = makeMountPath usbDevice; + in + { + keyFile = "${usbMountPath}/${keyPath}"; + keyFileTimeout = 1; + }) + cfg.devices; + } + ); +}