355 lines
11 KiB
Markdown
355 lines
11 KiB
Markdown
# Notes for 14th of March, 2023
|
||
|
||
Today was the day I finally got to setting up both "erase your darlings"
|
||
and proper disk encryption. This general setup concept utilizes NixOS'
|
||
ability to boot off of a disk that contains only `/nix` and `/boot`, linking
|
||
appropriate devices and blocks during the boot process and deleting all state
|
||
that programs may have left over my system.
|
||
|
||
The end result, for me, was a fully encrypted that uses btrfs
|
||
snapshots to restore `/` to its original state on each boot.
|
||
|
||
## Resources
|
||
|
||
- [This discourse post](https://discourse.nixos.org/t/impermanence-vs-systemd-initrd-w-tpm-unlocking/25167)
|
||
- [This blog post](https://elis.nu/blog/2020/06/nixos-tmpfs-as-home)
|
||
- [This other blog post](https://guekka.github.io/nixos-server-1/)
|
||
- [And this post that the previous post is based on](https://mt-caret.github.io/blog/posts/2020-06-29-optin-state.html)
|
||
- [Impermanence](https://github.com/nix-community/impermanence)
|
||
|
||
## The actual set-up (and reproduction steps)
|
||
|
||
I've had to go through a few guides before I could figure out a set up that I
|
||
really like. The final decision was that I would have an encrypted disk that
|
||
restores itself to its former state during boot. Is it fast? Absolutely not.
|
||
But it sure as hell is cool. And stateless!
|
||
|
||
To return the root (and only the root) we use a systemd service that fires
|
||
shortly after the disk is encrypted but before the root is actually mounted.
|
||
That way, we can unlock the disk, restore the disk to its pristine state
|
||
using the snapshot we have taken during installation and mount the root to
|
||
go on with our day.
|
||
|
||
### Reproduction steps
|
||
|
||
#### Partitioning
|
||
|
||
First you want to format your disk. If you are really comfortable with
|
||
bringing parted to your pre-formatted disks, by all means feel free to skip
|
||
this section. I, however, choose to format a fresh disk.
|
||
|
||
Start by partitioning the sections of our disk (sda1, sda2 and sda3)
|
||
_Device names might change if you're using a nvme disk, i.e nvme0p1._
|
||
|
||
```bash
|
||
# Set the disk name to make it easier
|
||
DISK=/dev/sda # replace this with the name of the device you are using
|
||
|
||
# set up the boot partition
|
||
parted "$DISK" -- mklabel gpt
|
||
parted "$DISK" -- mkpart ESP fat32 1MiB 1GiB
|
||
parted "$DISK" -- set 1 boot on
|
||
|
||
mkfs.vfat -n BOOT "$DISK"1
|
||
```
|
||
|
||
```bash
|
||
# set up the swap partition
|
||
parted "$DISK" -- mkpart Swap linux-swap 1GiB 9GiB
|
||
mkswap -L SWAP "$DISK"2
|
||
swapon "$DISK"2
|
||
```
|
||
|
||
_I do in fact use swap in the civilized year of 2023[^1]. If I were a little
|
||
more advanced, and if I did not disable hibernation due to overly-hardened
|
||
kernel parameters, I would also be encrypting the swap to secure the hibernates...
|
||
but that is *currently* out of my scope. You may find this desirable, however, I
|
||
will not be providing instructions on that._
|
||
|
||
Encrypt your partition, and open it to make it available under `/dev/mapper/enc`.
|
||
|
||
```bash
|
||
cryptsetup --verify-passphrase -v luksFormat "$DISK"3 # /dev/sda3
|
||
cryptsetup open "$DISK"3 enc
|
||
```
|
||
|
||
Now partition the encrypted device block.
|
||
|
||
```bash
|
||
parted "$DISK" -- mkpart primary 9GiB 100%
|
||
mkfs.btrfs -L NIXOS /dev/mapper/enc
|
||
```
|
||
|
||
```bash
|
||
mount -t btrfs /dev/mapper/enc /mnt
|
||
|
||
# First we create the subvolumes, those may differ as per your preferences
|
||
btrfs subvolume create /mnt/root
|
||
btrfs subvolume create /mnt/home
|
||
btrfs subvolume create /mnt/nix
|
||
btrfs subvolume create /mnt/persist # some people may choose to put /persist in /mnt/nix, I am not one of those people.
|
||
btrfs subvolume create /mnt/log
|
||
```
|
||
|
||
Now that we have created the btrfs subvolumes, it is time for the _readonly_
|
||
snapshot of the root subvolume.
|
||
|
||
```bash
|
||
btrfs subvolume snapshot -r /mnt/root /mnt/root-blank
|
||
|
||
# Make sure to unmount, or nixos-rebuild will try to remove /mnt and fail
|
||
umount /mnt
|
||
```
|
||
|
||
#### Mounting
|
||
|
||
After the subvolumes are created, we mount them with the options that we want.
|
||
Ideally, on NixOS, you want the `noatime` option [^2] and zstd
|
||
compression, especially on your `/nix` partition.
|
||
|
||
The following is my partition layout. If you have created any other subvolumes
|
||
in the step above, you will also want to mount them here. Below setup assumes
|
||
that you have been following the steps as is.
|
||
|
||
```bash
|
||
# /
|
||
mount -o subvol=root,compress=zstd,noatime /dev/mapper/enc /mnt
|
||
|
||
# /home
|
||
mkdir /mnt/home
|
||
mount -o subvol=home,compress=zstd,noatime /dev/mapper/enc /mnt/home
|
||
|
||
# /nix
|
||
mkdir /mnt/nix
|
||
mount -o subvol=nix,compress=zstd,noatime /dev/mapper/enc /mnt/nix
|
||
|
||
# /persist
|
||
mkdir /mnt/persist
|
||
mount -o subvol=persist,compress=zstd,noatime /dev/mapper/enc /mnt/persist
|
||
|
||
# /var/log
|
||
mkdir -p /mnt/var/log
|
||
mount -o subvol=log,compress=zstd,noatime /dev/mapper/enc /mnt/var/log
|
||
|
||
# do not forget to mount the boot partition
|
||
mkdir /mnt/boot
|
||
mount "$DISK"1 /mnt/boot
|
||
```
|
||
|
||
And finally let NixOS generate the hardware configuration.
|
||
|
||
```bash
|
||
nixos-generate-config --root /mnt
|
||
```
|
||
|
||
The genereated configuration will be available at `/mnt/etc/nixos`.
|
||
|
||
Before we move on, we need to add the `neededForBoot = true;` to some mounted
|
||
subvolumes in `hardware-configuration.nix`. It will look something like this:
|
||
|
||
```nix
|
||
# Do not modify this file! It was generated by ‘nixos-generate-config’
|
||
# and may be overwritten by future invocations. Please make changes
|
||
# to /etc/nixos/configuration.nix instead.
|
||
{
|
||
config,
|
||
lib,
|
||
pkgs,
|
||
modulesPath,
|
||
...
|
||
}: {
|
||
imports = [
|
||
(modulesPath + "/installer/scan/not-detected.nix")
|
||
];
|
||
|
||
boot.initrd.availableKernelModules = ["xhci_pci" "ahci" "usb_storage" "sd_mod" "rtsx_pci_sdmmc"];
|
||
boot.initrd.kernelModules = [];
|
||
boot.kernelModules = ["kvm-intel"];
|
||
boot.extraModulePackages = [];
|
||
|
||
fileSystems."/" = {
|
||
device = "/dev/disk/by-uuid/b79d3c8b-d511-4d66-a5e0-641a75440ada";
|
||
fsType = "btrfs";
|
||
options = ["subvol=root"];
|
||
};
|
||
|
||
boot.initrd.luks.devices."enc".device = "/dev/disk/by-uuid/82144284-cf1d-4d65-9999-2e7cdc3c75d4";
|
||
|
||
fileSystems."/home" = {
|
||
device = "/dev/disk/by-uuid/b79d3c8b-d511-4d66-a5e0-641a75440ada";
|
||
fsType = "btrfs";
|
||
options = ["subvol=home"];
|
||
};
|
||
|
||
fileSystems."/nix" = {
|
||
device = "/dev/disk/by-uuid/b79d3c8b-d511-4d66-a5e0-641a75440ada";
|
||
fsType = "btrfs";
|
||
options = ["subvol=nix"];
|
||
};
|
||
|
||
fileSystems."/persist" = {
|
||
device = "/dev/disk/by-uuid/b79d3c8b-d511-4d66-a5e0-641a75440ada";
|
||
fsType = "btrfs";
|
||
options = ["subvol=persist"];
|
||
neededForBoot = true; # <- add this
|
||
};
|
||
|
||
fileSystems."/var/log" = {
|
||
device = "/dev/disk/by-uuid/b79d3c8b-d511-4d66-a5e0-641a75440ada";
|
||
fsType = "btrfs";
|
||
options = ["subvol=log"];
|
||
neededForBoot = true; # <- add this
|
||
};
|
||
|
||
fileSystems."/boot" = {
|
||
device = "/dev/disk/by-uuid/FDED-3BCF";
|
||
fsType = "vfat";
|
||
};
|
||
|
||
swapDevices = [
|
||
{device = "/dev/disk/by-uuid/0d1fc824-623b-4bb8-bf7b-63a3e657889d";}
|
||
# if you encrypt your swap, it'll also need to be configured here
|
||
];
|
||
|
||
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||
powerManagement.cpuFreqGovernor = lib.mkDefault "powersave";
|
||
}
|
||
```
|
||
|
||
Do keep in mind that the NixOS hardware scanner **cannot** pick up your mount
|
||
options. Which means that you should specifiy the options (i.e `noatime`) for
|
||
each btrfs volume that you have created in `hardware-configuration.nix`. You
|
||
can simply add them in the `options = [ ]` list in quotation marks. I
|
||
recommend adding at least zstd compression, and optionally `noatime`.
|
||
|
||
### Closing Notes
|
||
|
||
And that should be all. By this point you are pretty much ready to install
|
||
with your existing config. I generally use my configuration flake to boot, so
|
||
there is no need to make any revisions. If you are starting from scratch, you
|
||
may consider tweaking your configuration.nix before you install the system.
|
||
An editor, such as Neovim, or your preferred DE/wm make good additions to your
|
||
configuration.
|
||
|
||
Once it's all done, take a deep breath and `nixos-install`. Once the
|
||
installation is done, you'll be prompted for the root password and after that
|
||
you can reboot. Now you are running NixOS on an encrypted disk. Nice!
|
||
|
||
Next up, if you are feeling _really_ fancy today, is to configure disk
|
||
erasure and impermanence.
|
||
|
||
#### Impermanence
|
||
|
||
For BTRFS snapshots, I use a systemd service that goes
|
||
|
||
```nix
|
||
boot.initrd.systemd = {
|
||
enable = true; # this enabled systemd support in stage1 - required for the below setup
|
||
services.rollback = {
|
||
description = "Rollback BTRFS root subvolume to a pristine state";
|
||
wantedBy = [
|
||
"initrd.target"
|
||
];
|
||
|
||
after = [
|
||
# LUKS/TPM process
|
||
"systemd-cryptsetup@enc.service"
|
||
];
|
||
|
||
before = [
|
||
"sysroot.mount"
|
||
];
|
||
|
||
unitConfig.DefaultDependencies = "no";
|
||
serviceConfig.Type = "oneshot";
|
||
script = ''
|
||
mkdir -p /mnt
|
||
|
||
# We first mount the btrfs root to /mnt
|
||
# so we can manipulate btrfs subvolumes.
|
||
mount -o subvol=/ /dev/mapper/enc /mnt
|
||
|
||
# While we're tempted to just delete /root and create
|
||
# a new snapshot from /root-blank, /root is already
|
||
# populated at this point with a number of subvolumes,
|
||
# which makes `btrfs subvolume delete` fail.
|
||
# So, we remove them first.
|
||
#
|
||
# /root contains subvolumes:
|
||
# - /root/var/lib/portables
|
||
# - /root/var/lib/machines
|
||
|
||
btrfs subvolume list -o /mnt/root |
|
||
cut -f9 -d' ' |
|
||
while read subvolume; do
|
||
echo "deleting /$subvolume subvolume..."
|
||
btrfs subvolume delete "/mnt/$subvolume"
|
||
done &&
|
||
echo "deleting /root subvolume..." &&
|
||
btrfs subvolume delete /mnt/root
|
||
echo "restoring blank /root subvolume..."
|
||
btrfs subvolume snapshot /mnt/root-blank /mnt/root
|
||
|
||
# Once we're done rolling back to a blank snapshot,
|
||
# we can unmount /mnt and continue on the boot process.
|
||
umount /mnt
|
||
'';
|
||
};
|
||
};
|
||
|
||
```
|
||
|
||
> You may opt in for `boot.initrd.postDeviceCommands = lib.mkBefore ''`
|
||
> as [this blog post](https://mt-caret.github.io/blog/posts/2020-06-29-optin-state.html)
|
||
> suggests. I am not exactly sure how exactly those options actually
|
||
> compare, however, a systemd service means it will be accessible through the
|
||
> the systemd service interface, which is why I opt-in for a service.
|
||
|
||
##### Implications
|
||
|
||
What this implies is that certain files such as saved networks for
|
||
network-manager will be deleted on each reboot. While a little clunky,
|
||
[Impermanence](https://github.com/nix-community/impermanence) is a great
|
||
solution to our problem.
|
||
|
||
Impermanence exposes to our system an `environment.persistence."<dirName>"` option that we can use to make certain directories or files permanent.
|
||
My module goes like this:
|
||
|
||
```nix
|
||
imports = [inputs.impermanence.nixosModules.impermanence]; # the import will be different if flakes are not enabled on your system
|
||
|
||
environment.persistence."/persist" = {
|
||
directories = [
|
||
"/etc/nixos"
|
||
"/etc/NetworkManager/system-connections"
|
||
"/etc/secureboot"
|
||
"/var/db/sudo"
|
||
];
|
||
|
||
files = [
|
||
"/etc/machine-id"
|
||
|
||
# ssh stuff
|
||
"/etc/ssh/ssh_host_ed25519_key"
|
||
"/etc/ssh/ssh_host_ed25519_key.pub"
|
||
"/etc/ssh/ssh_host_rsa_key"
|
||
"/etc/ssh/ssh_host_rsa_key.pub"
|
||
# if you use docker or LXD, also persist their directories
|
||
];
|
||
};
|
||
```
|
||
|
||
And that is pretty much it. If everything went well, you should now be telling
|
||
your friends about your new system boasting full disk encryption _and_ root
|
||
rollbacks.
|
||
|
||
## Why?
|
||
|
||
Honestly, why not?
|
||
|
||
[^1]:
|
||
I could be using `tmpfs` for `/` at this point in time. Unfortunately, since I share this setup on some of my low-end laptops, I've got no RAM
|
||
to spare - which is exactly why I have opted out with BTRFS. It is a reliable filesystem that I am used to, and it allows for us to use a script
|
||
that we'll see later on.
|
||
|
||
[^2]: https://opensource.com/article/20/6/linux-noatime
|