This repository contains a reproducible NixOS configuration using flakes.
Based on the NixOS manual.
Download the NixOS minimal ISO from nixos.org/download and write it to a USB drive:
dd if=nixos-minimal-*-x86_64-linux.iso of=/dev/sdX bs=4M status=progressBoot from the USB stick. You will be logged in as root automatically.
The on-disk layout is declared in modules/nixos/disko.nix and the LUKS+TPM2 unlock and hibernation in their own modules. New hosts opt in by importing them and setting two values:
/dev/<disk>
├── p1 ESP 512 MiB FAT32 → /boot
└── p2 LUKS rest of disk
└── LVM VG "main"
├── lv "swap" RAM + sqrt(RAM) MiB (encrypted, hibernate-ok)
└── lv "root" 100%FREE btrfs → @ / @home / @nix
(compress=zstd:3, noatime, ssd)
On a machine with this repo checked out, copy an existing host as a starting point:
HOST=<hostname>
cp -r modules/hosts/i145420 modules/hosts/$HOSTBoot the live ISO on the target machine and dump its detected hardware so you can pick the right boot.initrd.availableKernelModules / boot.kernelModules / CPU-microcode lines for the new host. The simplest way is to push it to a paste service:
nixos-generate-config --no-filesystems --root /tmp/scan
curl --data-binary @/tmp/scan/etc/nixos/hardware-configuration.nix https://paste.rsEdit modules/hosts/$HOST/hardware.nix accordingly. Disko owns the filesystem layout, so the file should contain only kernel modules, CPU/microcode, and nixpkgs.hostPlatform — no fileSystems and no swapDevices blocks.
Edit modules/hosts/$HOST/default.nix so the imports include the new modules:
imports = [
nixos.${HOST}-hardware
nixos.${HOST}-configuration
nixos.common
nixos.disko
nixos.secureboot
nixos.luks-tpm2
nixos.hibernation
# nixos.laptop / nixos.fingerprint / nixos.u2f / nixos.gaming as appropriate
];In modules/hosts/$HOST/configuration.nix, set the per-host disk values:
peteyycz.disk = {
device = "/dev/nvme0n1"; # whole disk to wipe
swapSizeMiB = 16512; # RAM_MiB + ceil(sqrt(RAM_MiB))
bootMaskMode = "0022"; # or "0077"
};Compute swapSizeMiB from RAM:
awk '/MemTotal/ {r=int($2/1024); printf "%d\n", r + int(sqrt(r) + 0.999)}' /proc/meminfoCommit and push to master before installing.
Back on the NixOS USB, partition the disk:
sudo nix --experimental-features 'nix-command flakes' run \
github:nix-community/disko/latest -- \
--mode destroy,format,mount \
--flake github:peteyycz/dotfiles#<hostname>disko --mode destroy,format,mount wipes the disk, creates the GPT layout, opens LUKS (you will set the passphrase here), creates the LVM volumes, formats btrfs, and mounts everything under /mnt.
Before installing, generate Secure Boot keys into the new system so lanzaboote has something to sign with on first boot:
sudo mkdir -p /mnt/var/lib/sbctl
sudo mount --bind /mnt/var/lib/sbctl /var/lib/sbctl
sudo nix --experimental-features 'nix-command flakes' run \
nixpkgs#sbctl -- create-keys
sudo umount /var/lib/sbctlThe bind mount redirects sbctl's hard-coded /var/lib/sbctl path into the target filesystem, so the keys land where the installed system will look for them. Now install:
sudo nixos-install --no-root-passwd --flake github:peteyycz/dotfiles#<hostname>nixos-install writes the system and lanzaboote signs the bootloader, kernel, and initrd with the keys you just generated.
rebootBoot will prompt for the LUKS passphrase. After login:
sudo passwd peteyyczThe secureboot module replaces systemd-boot with lanzaboote so the bootloader, kernel, and initrd are all signed. Binding the LUKS TPM2 keyslot to PCR 7 (Secure Boot policy) then gives a stable measurement that survives firmware updates — but is only meaningful once Secure Boot is actually enforcing.
First confirm the firmware is in Setup Mode (no Platform Key enrolled yet — required so sbctl enroll-keys can adopt your generated keys):
bootctl status | grep -i 'secure boot' # → "disabled (setup)"If it instead reports enabled or disabled without (setup), reboot into firmware and clear/reset the Secure Boot keys. On Lenovo: Security → Secure Boot → Reset to Setup Mode.
Enroll the keys generated during step 3 and verify everything in /boot is signed:
sudo sbctl enroll-keys --microsoft # keep MS keys so option ROMs still verify
sudo sbctl verify # all listed files should report "signed"If enroll-keys aborts with File is immutable: /sys/firmware/efi/efivars/..., the kernel
has set the immutable bit on those EFI variables. Clear it and re-run — sbctl stops at the
first failures, so clear KEK, db and dbx, not just the ones it named:
sudo chattr -i /sys/firmware/efi/efivars/{KEK,db,dbx}-*The bit is reapplied on every boot, so this is not a persistent change — expect to repeat it if you ever re-enroll.
Reboot into firmware setup, enable Secure Boot, and boot back in. Verify:
bootctl status | grep -i 'secure boot' # → "enabled (user)" or "enabled (deployed)"Either mode is fine. Deployed Mode is User Mode plus a firmware lock against dropping back to Setup Mode without a signed update; PCR 7 measurement is identical. It only matters if you later need to re-enroll keys, which then requires clearing the Platform Key from firmware first.
Now enroll the TPM2 keyslot bound to PCR 7. The LUKS partition is the second partition created by disko (/dev/<disk>p2):
sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7 /dev/nvme0n1p2Reboot — LUKS now unlocks unattended via TPM2. PCR 7 only changes when Secure Boot keys or policy change, so firmware updates and kernel upgrades won't break the unlock.
Re-enrolling later. If PCR 7 changes (you toggled Secure Boot, re-enrolled keys, etc.), drop back to the passphrase at boot, then check the LUKS header before doing anything destructive:
sudo cryptsetup luksDump /dev/nvme0n1p2Note the keyslot index of the existing TPM2 token (look for systemd-tpm2 in the Tokens section), then wipe that specific slot number and re-enroll:
sudo cryptsetup luksKillSlot /dev/nvme0n1p2 <N>
sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7 /dev/nvme0n1p2Avoid systemd-cryptenroll --wipe-slot=tpm2 — its targeting can drift if tokens are stale, and a mistake there will wipe your passphrase keyslot too. Always pass an explicit slot number.
sudo fprintd-enrollFingerprint is used for sudo and the Plasma lockscreen. The SDDM greeter is deliberately password-only so pam_kwallet can unlock the wallet with the login password — a fingerprint login leaves it locked and kwallet prompts separately, costing two auth steps instead of one. Fingerprint is not used for LUKS unlock at boot either (fprintd-class readers aren't supported in the initrd).
After making changes to the configuration, rebuild with:
sudo nixos-rebuild switch --flake /path/to/dotfiles#<hostname>