Secure Boot on NixOS
Attesting physical security of NixOS with Secure Boot and TPM2.
5 min readThis post builds on configuration in Encrypted Root and ZFS on NixOS.
Physical security of devices is important to protect the integrity of data and prevent it from falling into the wrong hands. In the last post, we greatly improved physical security by encrypting data at rest on drives. It wasn’t without negatives though, as we now need to provide a password every time the machine boots, making it challenging to be physically distant from the machine, or annoying to hook up a keyboard and monitor every time a reboot is necessary.
This isn’t something we’ll need to live with long. Microsoft released Secure Boot in 2011, and it’s now present on effectively every machine. The technology has its criticisms, but in this scenario, we can leverage it to our advantage.
Secure Boot requires that the bootloader is signed by a trusted key, and we are able to install our own key into the UEFI. This effectively helps us know that the machine is always booting into a Linux kernel we’ve installed. Adding to this is the Trusted Platform Module (TPM) which stores measurements collected about the system, as well as cryptographic keys or other secrets.
Combining Secure Boot and TPM2, we’re able to effectively attest that the system has booted with a UEFI, bootloader, kernel parameters, and more, that we trust. If that attestation is successful, the TPM can release a secret to the system that can be used to automatically unlock our encrypted root partition.
Creating Secure Boot Keys
To create the secure boot signing key, ssh into the remote machine and run
sbctl
. We don’t have this tool installed yet, but we can run it in a Nix
shell.
nix-shell -p sbctl
sbctl create-keys
This will place the keys in either /etc/secureboot
or /var/lib/sbctl
depending on the version of sbctl you have. Make a note of this, as we’ll need
to configure impermanence to keep the directory around.
Setup Lanzaboote
Lanzaboote is the tool that enables Secure Boot for NixOS installs. To get
started, let’s take a dependency on it in our flake.nix
.
inputs = {
# ...
# secure-boot
lanzaboote = {
url = "github:nix-community/lanzaboote/v0.4.1";
inputs.nixpkgs.follows = "nixpkgs";
inputs.flake-utils.follows = "flake-utils";
};
};
# ...
nixosConfigurations = {
kube-host-1 = lib.nixosSystem {
system = "x86_64-linux";
modules = [
lanzaboote.nixosModules.lanzaboote
impermanence.nixosModules.impermanence
./nix/hosts/kube-host-1
];
specialArgs = {inherit inputs outputs;};
};
};
Secure Boot will require some configuration and a few tools on the remote
machine, so let’s create a new module. We’ll start by updating
nix/modules/server/default.nix
.
{
# ...
}: {
imports = [
./secure-boot.nix
./zfs.nix
];
# ...
config = lib.mkIf cfg.enable {
rs-homelab.server.secure-boot.enable = lib.mkDefault true;
rs-homelab.server.zfs.enable = lib.mkDefault true;
};
}
Here we’re just importing the module we’re about to create, and enabling
secure-boot configuration by default. If you’ve been following along, this will
get enabled automatically. Otherwise, be sure to set
rs-homelab.server.enable = true;
. Let’s add the module now at
nix/modules/server/secure-boot.nix
.
{
lib,
config,
pkgs,
...
}: let
cfg = config.rs-homelab.server.secure-boot;
in {
options = {
rs-homelab.server.secure-boot.enable = lib.mkEnableOption "Enables secure boot";
};
config = lib.mkIf cfg.enable {
boot.lanzaboote = {
enable = true;
pkiBundle = "/var/lib/sbctl"; # or /etc/secureboot
};
# or /etc/secureboot
environment.persistence."/persist".directories = ["/var/lib/sbctl"];
# Disable systemd-boot because lanzaboote installs the signed bootloader
boot.loader.systemd-boot.enable = lib.mkForce false;
boot.initrd.systemd.enable = true;
boot.initrd.systemd.tpm2.enable = true;
security.tpm2 = {
enable = true;
pkcs11.enable = true;
tctiEnvironment.enable = true;
};
environment.shellAliases = {
# Requires a device argument (/dev/nvme1n1p2)
cryptenroll = "systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=0+2+7+15:sha256=0000000000000000000000000000000000000000000000000000000000000000 --wipe-slot=tpm2";
};
# CLI tools to debug with
environment.systemPackages = with pkgs; [
sbctl
tpm2-tools
];
};
}
This adds TPM2 support for the system, as well as to the boot process. It also installs sbctl on the remote machine, and a shell alias that will come in handy when we need to re-enroll the TPM due to new measurements. Finally, it enables lanzaboote, which will automatically pick up the signing key on the remote machine, sign the bootloader, and install it.
Last but not least, we need to enable TPM measurement support on the LUKS device
itself in nix/hosts/kube-host-1/hardware-configuration.nix
.
boot.initrd.luks.devices = {
luks-rpool = {
# ...
crypttabExtraOpts = ["tpm2-device=auto" "tpm2-measure-pcr=yes"];
};
};
Now we’re ready to apply the configuration onto the remote machine. Before we
move onto enabling Secure Boot, run sbctl verify
on the remote machine to make
sure the nixos-generation-*.efi
files are signed, as well as
systemd-bootx64.efi
.
Enabling Secure Boot
Check the quick start guide in the lanzaboote repository and follow those instructions.
Don’t forget to set a password on the BIOS. Secure Boot isn’t as helpful if it can be turned off easily. The TPM will still fail to unlock the encrypted partition automatically if this occurs.
On the MS-01, you will also need to go into the “Security” > “Key Management” menu and disable “Factory Key Provision”. Make sure to save changes when resetting, as there is a pop-up menu that asks to reset without saving changes.
Auto-Unlock the LUKS Partition
Now that secure boot is enabled, TPM measurements should be stable and we can lock them in by enrolling the LUKS-encrypted partition to automatically decrypt. On the remote machine, run:
# replace sdxN with the encrypted partition (sda2, nvme0n1p2, etc.)
cryptenroll /dev/sdxN
Enter the password you use to decrypt the partition at boot. With the device enrolled, we can try it out by rebooting. If you need to enter your password, try enrolling the device again.
Conclusion
Our physical security posture is now in a much better position. Should the machine get stolen, attackers will have a much harder time reading data directly off the disk, or tampering with the boot process.
This post is one of many about running a homelab. To view more, click on a tag at the top of the page to see more related posts.