Installing Guix (Full LUKS + LVM)
Table of Contents
Here are some notes about installing Guix SD onto a new system from an existing configuration. We necessarily need to discuss disk preparation and partitioning. Additionally, we discuss from the perspective of existing configurations. I planned to write this back in 2022, after initially switching from NixOS to Guix. I hesitated at the time since such a post would not have any additional value to people otherwise installing Guix. However, going through the process again, I feel there are some comments to capture that may be beneficial to others venturing down this path. I will save the discussion between NixOS and Guix for another time. For now, let's settle on "scheme is more better."
After having a corrupted GRUB kill my previous Guix System, and a false sense of urgency, I have been running, begrudgingly, Fedora Workstation for the last 10 months. However, with summer here, I am eager to return to using Guix as my primary OS.
Fedora has been fine. It seems to work for the most part. But there just seems to be this constant encroaching entropy causing the system to become less and less stable. Using Guix as a package manager seemed to work fine in the beginning, but over time, it has started showing some degradation that is simply unworkable. If I must scorch earth reinstall, I might as well install using my preferred OS and existing system configuration. However, in my attempts to switch back to Guix, I have uncovered some sharp edges, because of course.
Disk preparation steps
First step to any fully encrypted setup is to throw a bunch of random bits onto
the disk, the zeroth step is to check it with badblocks
. Next, randomize the
data on the disk to protect against easy statistical analysis. Afterwards, we
partition the drive into two primary physical partitions. Then, we create the
LUKS container and the LVM volumes. Finally, we format all the different
volumes and mount them in preparation for system init
.
Badblocks
Since I still needed a working machine in the meantime, I purchased a second drive. This way, if the installation fails for any reason— it did— I can switch back to a working "machine". But to be sure the new drive is in good working order, I tested it with badblocks:
badblocks -wsv -t 0xEF ${device} badblocks -wsv -t 0XFE ${device}
This does two read-write, read DESTRUCTIVE, passes on the new device, checking for any errors.
Now for some, this is "enough" random scrubbing of the disk in preparation for an encrypted volume. However, I disagree. If the entire disk is a giant pattern of "0xFE" values, your actual data shines like a bright hot star for statistical analysis of the data.
Random "zeroing"
The fastest (not to be confused with "best") way I have seen to randomly write
garbage to a drive is to use a plain dm-crypt
device with a random passphrase
and write zeros to the resulting container.
cryptsetup open --type plain --key-file=/dev/urandom ${device} wipe-me dd if=/dev/zero of=/dev/mapper/wipe-me bs=4096 status=progress cryptsetup close
Also, critical for the throughput of the above command, setting the "blocksize"
via bs=4096
dramatically improves the write speed.
Partitioning
Since the system uses a single device and we have a "fully encrypted" setup, we only need two partitions: first for the EFI System Partition (ESP) and second for the LUKS container.
I created this using the interactive prompts within gptdisk
. Creating the
first partition to be 128M at the default starting sector (2048), and the
second taking the rest of the sectors. I denoted their partition types using
the codes EF00
, and 8308
, respectively. Finally, I renamed them to something
more meaningful to me: "boot" and "guix", respectively.
Creating LUKS container
With a fully encrypted system, including /boot
, there are some important
limitations to keep in mind. As of this writing, GRUB does not support LUKS2
containers nor does it support the Argon2 key derivation function. Both of
which are the default for recent versions of cryptsetup
.
This is tricky because GRUB does claims to support LUKS2, however, I was unable to get this to work.
cryptsetup --type luks1 \ --cipher aes-xts-plain64 \ --key-size 512 \ --hash sha512 \ --iter-time 7680 \ --use-random \ --pbkdf=pbkdf2 \ --verify-passphrase \ luksFormat \ ${device_part}
There are a few important notes here: first, the container type, as mentioned,
should be LUKS1 type container; second, the PBKDF needs to be PBKDF2, otherwise
GRUB does not know how to unlock it; third, make sure you replace
${device_part}
with the appropriate partition. While the initial unlocking is
quite slow because GRUB does not have full access to the machine, lowering the
iteration count is not advisable for a "secure" machine.
Ensure it all works by opening the container:
cryptsetup open ${device_part} cryptroot
A new device should be available under the /dev/mapper
tree.
"Correctly" naming the cryptroot
I do not think it may be important, but it is perhaps worth mentioning: the
name of the device should match the name used in your configuration. Use blkid
to get the UUID if needed.
cryptroot open ${device_part} \ luks-$(blkid | grep ${device_part} | awk '{print $2}' | sed 's/UUID=//' | sed 's/\"//g')
Creating the LVM containers
Now, we can create the LVM containers for our partitions.
pvcreate /dev/mapper/cryptroot vgcreate vg0 /dev/mapper/cryptroot lvcreate -L 1G vg0 -n root lvcreate -L 100G vg0 -n guix lvcreate -L 100G vg0 -n nix lvcreate -L 32G vg0 -n var lvcreate -L 32G vg0 -n opt lvcreate -L 32G vg0 -n tmp lvcreate -L 64G vg0 -n swap lvcreate -L 1T vg0 -n home
Since everything lives under /gnu/
, the root partition really doesn't need to
be even this large (I'm currently only using 18M on the root partition).
However, Guix currently warns about this during the initialization phase. We
can safely ignore this warning since we know the store writes go to a different
partition. As I recall, this was not a warning before, requiring the root
partition to be quite large for the initial installation.
Formatting
Now that we have all the partitions and logical volumes created, we need to
create actual "filesystems" for each. I have been using a combination of ext4
and xfs
for a while, and it seems to work well. While you may be able to get
away creating the ESP as an ext2
partition, it is probably easier and more
widely supported to format it using Fat32
.
Since version 5.15, xfs partitions default to including
bigtime
, avoiding the 2038 problem. Check back in 2486…
mkfs.vfat -F 32 -n boot ${ESP}
mkfs.ext4 -L root /dev/mapper/vg0-root
mkfs.ext4 -L var /dev/mapper/vg0-var
mkfs.ext4 -L opt /dev/mapper/vg0-opt
mkfs.ext4 -L tmp /dev/mapper/vg0-tmp
mkfs.xfs -L guix /dev/mapper/vg0-guix
mkfs.xfs -L nix /dev/mapper/vg0-nix
mkfs.xfs -L home /dev/mapper/vg0-home
Do not forget to format the swap volume:
mkswap /dev/mapper/vg0-swap
Mounting
The penultimate step before turning over to the initialization step is to mount each of the partitions:
mount /dev/mapper/vg0-root /mnt
mkdir -p /mnt/{boot/efi,gnu,nix,opt,var,tmp,home}
mount ${ESP} /mnt/boot/efi/
mount /dev/mapper/vg0-guix /mnt/gnu
mount /dev/mapper/vg0-nix /mnt/nix
mount /dev/mapper/vg0-var /mnt/var
mount /dev/mapper/vg0-opt /mnt/opt
mount /dev/mapper/vg0-tmp /mnt/tmp
mount /dev/mapper/vg0-home /mnt/home
You can optionally enable the swap partition for the current live installation:
swapon /dev/mapper/vg0-swap
Nix Store
If you choose to run nix
as an addition package manager, make sure to create
the "store" directory, lest your first boot waits forever for such a directory
to appear…
mkdir -p /mnt/nix/store
Cloning and modifying UUIDs
Each configuration is different, but before initializing the system, I need to
change the UUIDs of the initial LUKS volumes within my system configuration.
Some people have clever ways of scripting this out. I accept this tiny amount
of pain now which grants me the peace of mind I do not need to worry about it
some how changing and becoming unbootable later. I simply use blkid
to get the
UUID of the ${device_part}
and update the system config accordingly:
modified systems/axo.scm @@ -59,8 +59,8 @@ (mapped-devices (list (mapped-device - (source (uuid "f1e8d842-1c63-4311-803d-938f31d48d49")) - (target "luks-f1e8d842-1c63-4311-803d-938f31d48d49") + (source (uuid "d1bcf4fd-8fe8-41b6-88dc-c83851b1f071")) + (target "luks-d1bcf4fd-8fe8-41b6-88dc-c83851b1f071") (type luks-device-mapping)) (mapped-device (source "vg0") @@ -117,7 +118,7 @@ (needed-for-boot? #t) (dependencies mapped-devices))) (efi (file-system - (device (uuid "5A5D-20AF" 'fat)) + (device (uuid "0601-7942" 'fat)) (mount-point "/boot/efi") (type "vfat") (dependencies mapped-devices))))
System initialization
Before starting the initialization process, start the copy-on-write service to ensure any changes made to the running Guix system (e.g., live installation media) are copied to the new system's store.
herd start cow-store /mnt
Now, we can move to initializing the system.
It's critical for this step that your system has network connectivity. Test by pinging CloudFlare:
ping 1.1.1.1
.
From the root of the https://git.sr.ht/~kennyballou/dotfiles.git repository, run the following command:
guix time-machine -C ./config/guix/channels.scm -- system init \ -L ./ \ systems/${machine}.scm \ /mnt
Assuming everything works, we should be able to reboot into the new system.
After initialization steps
After successfully booting into the freshly initialized system, we need to set the passwords of the root and default user.
Switch over to a different tty
and "login" as root, and set the passwords using
passwd
.
Parting thoughts
Because installing my typical configuration is a little heavy, I am considering creating a smaller configuration just for the initial installation to quickly get into the new system. From there, re-configuring to the final system is a lot easier (and safer) than the installation media.