notesassorted ramblings on computer

Replicating the Guix Store in /boot

While Guix supports full disk encryption, it requires the partition holding the Store to be unlocked by Grub. This is the case because Guix’s default Grub configuration references the Kernel, initramfs, … by their full store file name. Unfortunately, Grub’s LUKS support is somewhat limited:

  1. It’s often lacking behind cryptsetup upstream it terms of features. For example, Grub only gained support for LUKS2 recently.
  2. Grub only supports a limited amount of key derivation functions. Importantly, it does not support argon2id, which is the default for LUKS2.
  3. Grub’s implementation is slow, decryption of volumes with multiple keyslots can take a while.

In order to mitigate these drawbacks, I would like to decrypt my LUKS volume from the initramfs (thereby using cryptsetup instead of Grub). To do so, I need to ensure that Kernel, initramfs, … are stored and loaded from an unencrypted /boot partition.

The grub-copy wrapper

To implement this setup, we need to copy all files required by Grub to /boot. Guix supports different Grub configuration. Ideally, we want a solution that supports these different configurations. Therefore, the following code implements a grub-copy procedure that wraps an existing bootloader-installer:

;; wraps an existing Grub 'bootloader-installer' in a procedure which copies
;; all files referenced in Grub's configuration file to the install directory.
(define (grub-copy installer)
  #~(lambda (bootloader device mount-point)
      (use-modules ((srfi srfi-1) #:select (fold lset-adjoin lset-difference))
                   ((guix import utils) #:select (read-lines))
                   ((guix store) #:select (direct-store-path))
                   (ice-9 regex))

      ;; regex for finding a path to the Store in the Grub configuration file.
      ;; Obviously a heurstic, ideally we would get this information from Grub.
      (define store-regexp (make-regexp "/gnu/store/[A-Za-z0-9@/._-]+"))

      ;; regex for finding the linux command in a Grub configuration file.
      ;; See https://www.gnu.org/software/grub/manual/grub/grub.html#linux
      (define linux-regexp (make-regexp "^[[:space:]]*linux[[:space:]]"))

      ;; Takes a list of /gnu/store paths and returns a list of unique directory
      ;; entries in the /gnu/store directory (usually: hash + package + version).
      (define (store-entries paths)
        (fold
          (lambda (path acc)
            (lset-adjoin
              equal?
              acc
              (substring (direct-store-path path) (string-length "/gnu/store"))))
          '() paths))

      (define (required-paths lines)
        (fold
          (lambda (line acc)
            (let ((paths (map match:substring (list-matches store-regexp line))))
              (if (null? paths)
                acc
                (if (regexp-exec linux-regexp line) ; ignore kernel cmdline paths
                  (lset-adjoin equal? acc (car paths))
                  (apply lset-adjoin (cons* equal? acc paths))))))
          '() lines))

      (define (existing-paths store)
        (map
          (lambda (path)
            (substring path (string-length "/boot")))
          (find-files store)))

      (let* ((install-dir (canonicalize-path (string-append mount-point "/boot")))
             (grub-cfg (string-append install-dir "/grub/grub.cfg"))
             (grub-lines (call-with-input-file grub-cfg read-lines))
             (required (required-paths grub-lines))
             (existing (existing-paths (string-append install-dir "/gnu/store"))))
        (for-each ; remove leftovers from old generations
          (lambda (store-entry)
            (delete-file-recursively
              (string-append install-dir "/gnu/store/" store-entry)))
          (lset-difference equal? (store-entries existing) (store-entries required)))
        (for-each ; copy required files
          (lambda (store-file)
            (let ((dest-file (string-append install-dir store-file)))
              (mkdir-p (dirname dest-file))
              (copy-recursively store-file dest-file)))
          required))

      ;; Invoke the 'bootloader-installer' that we are wrapping.
      (#$installer bootloader device mount-point)))

The implementation is a bit hacky, it extracts references to Guix’s store from an existing grub.cfg using regular expressions. This is necessary as Guix’s bootloader setup uses separate functions for generating the bootloader configuration (configuration-file-generator) and installing the bootloader (installer). While the former receives a menu-entry abstraction as an input, from which we could theoretically extract store paths, it must return a computed-file and cannot copy files to /boot.

Usage of grub-copy

Usage of grub-copy is relatively straight forward. Consider the following example which wraps the existing grub-bootloader for BIOS-based Grub booting:

(define grub-copy-bootloader
  (bootloader
    (inherit grub-bootloader)
    (installer (grub-copy (bootloader-installer grub-bootloader)))))

However, grub-copy also works with other bootloader configurations (e.g., grub-efi-bootloader).

Prior Art

This draws inspiration from Rutherther’s guix-config. Specifically, his grub-efi-copy-bootloader bootloader configuration from the (ruther bootloader grub) module. However, contrary to his implementation, grub-copy works (a) with any existing Guix bootloader configuration and (b) also supports removing leftovers from deleted generations.

More Information