Add a read-only appliance image with a dm-verity protected store
nixosModules.image builds a GPT disk image with systemd-repart: ESP with a UKI carrying the store's verity root hash, an erofs /nix/store on a dm-verity partition, a tmpfs root, an immutable /etc overlay and an ext4 state partition on /var. No Nix on the device; it is replaced as a whole. The store is made with 4 KiB erofs blocks: systemd-repart passes the 512 byte sector size to mkfs.erofs, and libblkid cannot verify the checksum of such a superblock, so udev never reported the file system and the initrd waited forever for /dev/mapper/usr. examples/image (nix build .#image) is a hardware test build with a debug console password; checks.image boots the image under UEFI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
# 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;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user