# Appliance image: a GPT disk image with a dm-verity protected, read-only # /nix/store partition, a volatile (tmpfs) root and a small persistent state # partition for /var. Built with systemd-repart; the unified kernel image on the # ESP carries the hash of the store partition, so a modified store does not boot. # # The system cannot be rebuilt on the device (no Nix); it is replaced as a whole. # Importing this module turns a configuration into such an image # (`config.system.build.image`). { config, lib, pkgs, modulesPath, ... }: let cfg = config.router.image; inherit (config.image.repart.verityStore) partitionIds; efiArch = pkgs.stdenv.hostPlatform.efiArch; in { imports = [ (modulesPath + "/image/repart.nix") (modulesPath + "/profiles/image-based-appliance.nix") (modulesPath + "/profiles/perlless.nix") ]; options.router.image = { espSize = lib.mkOption { type = lib.types.str; default = "128M"; description = "Size of the EFI system partition (holds the unified kernel image)."; }; stateSize = lib.mkOption { type = lib.types.str; default = "512M"; description = "Minimum size of the persistent state partition mounted on /var."; }; }; config = { system.image.id = lib.mkDefault "nixos-router"; # No Nix on the device: the rollback helpers need it, and boot counting # takes over their job in images. router.rollback.enable = false; boot.loader.grub.enable = false; fileSystems = { "/" = { fsType = "tmpfs"; options = [ "mode=0755" "size=50%" ]; }; # The only persistent, writable place: DHCP leases, SSH host keys, # journal, secrets (/var/lib/router). "/var" = { device = "/dev/disk/by-partlabel/state"; fsType = "ext4"; options = [ "nosuid" "nodev" ]; }; }; # /etc is a read-only overlay; users are created by userborn, which keeps # its password files on the state partition. system.etc.overlay.mutable = false; services.userborn.passwordFilesLocation = "/var/lib/nixos"; # Host keys live on the state partition as well. services.openssh.hostKeys = [ { path = "/var/lib/ssh/ssh_host_ed25519_key"; type = "ed25519"; } ]; # /usr is the read-only verity mount; there is nothing to create there. system.activationScripts.usrbinenv = lib.mkForce ""; image.repart = { enable = true; # systemd-repart passes the image's sector size (512) to mkfs.erofs as the # block size; libblkid cannot verify the superblock checksum of an erofs # with blocks <= 1 KiB, so udev never reports the store's file system and # systemd waits forever for /dev/mapper/usr. Appended options win. mkfsOptions.erofs = [ "-b4096" ]; verityStore = { enable = true; # The firmware's default boot path: no NVRAM entry needed. ukiPath = "/EFI/BOOT/BOOT${lib.toUpper efiArch}.EFI"; }; partitions = { ${partitionIds.esp}.repartConfig = { Type = "esp"; Format = "vfat"; SizeMinBytes = cfg.espSize; }; "30-state".repartConfig = { Type = "var"; Format = "ext4"; Label = "state"; SizeMinBytes = cfg.stateSize; }; }; }; }; }