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:
+104
@@ -0,0 +1,104 @@
|
||||
# The appliance image: boot the raw disk image under UEFI and check what makes
|
||||
# it an appliance (read-only verity store, volatile root, persistent /var) while
|
||||
# the router itself runs.
|
||||
{ self, pkgs }:
|
||||
let
|
||||
common = import ./common.nix;
|
||||
in
|
||||
pkgs.testers.runNixOSTest {
|
||||
name = "router-image";
|
||||
|
||||
nodes.machine =
|
||||
{ lib, ... }:
|
||||
{
|
||||
imports = [
|
||||
self.nixosModules.router
|
||||
self.nixosModules.image
|
||||
common.tools
|
||||
];
|
||||
|
||||
# The VM boots the image's own disk, not the usual test plumbing.
|
||||
virtualisation.fileSystems = lib.mkVMOverride { };
|
||||
virtualisation.directBoot.enable = false;
|
||||
virtualisation.mountHostNixStore = false;
|
||||
virtualisation.useEFIBoot = true;
|
||||
virtualisation.vlans = [
|
||||
1
|
||||
2
|
||||
];
|
||||
networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ ];
|
||||
networking.interfaces.eth2.ipv4.addresses = lib.mkForce [ ];
|
||||
|
||||
system.image.version = "1";
|
||||
|
||||
router = {
|
||||
enable = true;
|
||||
wan.interface = "eth1";
|
||||
lans.main = {
|
||||
interface = "eth2";
|
||||
internet = true;
|
||||
ipv4.address = "192.168.10.1";
|
||||
dhcp.pool = {
|
||||
start = "192.168.10.100";
|
||||
end = "192.168.10.199";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript =
|
||||
{ nodes, ... }:
|
||||
''
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
disk = tempfile.NamedTemporaryFile()
|
||||
subprocess.run([
|
||||
"${nodes.machine.virtualisation.qemu.package}/bin/qemu-img", "create",
|
||||
"-f", "qcow2",
|
||||
"-b", "${nodes.machine.system.build.image}/${nodes.machine.image.filePath}",
|
||||
"-F", "raw", disk.name,
|
||||
], check=True)
|
||||
os.environ["NIX_DISK_IMAGE"] = disk.name
|
||||
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
|
||||
with subtest("the disk has the appliance layout"):
|
||||
parts = machine.succeed("lsblk -n -o PARTLABEL /dev/vda")
|
||||
for label in ("esp", "store-verity", "store", "state"):
|
||||
assert label in parts, parts
|
||||
|
||||
with subtest("root is volatile and /nix/store is a dm-verity protected read-only partition"):
|
||||
machine.succeed("findmnt --kernel --type tmpfs /")
|
||||
assert "ACTIVE" in machine.succeed("dmsetup info --target verity usr")
|
||||
backing = machine.succeed("df --output=source /nix/store | tail -n1").strip()
|
||||
assert backing == "/dev/mapper/usr", backing
|
||||
machine.fail("touch /nix/store/should-not-work")
|
||||
machine.fail("touch /etc/should-not-work")
|
||||
|
||||
with subtest("the image identifies itself"):
|
||||
machine.succeed("grep -qE '^IMAGE_VERSION=.?1.?$' /etc/os-release")
|
||||
machine.succeed("grep -qE '^IMAGE_ID=.?nixos-router.?$' /etc/os-release")
|
||||
|
||||
with subtest("the router comes up from the image"):
|
||||
machine.wait_for_unit("nftables.service")
|
||||
machine.wait_for_unit("unbound.service")
|
||||
machine.wait_for_unit("kea-dhcp4-server.service")
|
||||
machine.wait_until_succeeds("ip -4 addr show dev eth2 | grep -q 192.168.10.1")
|
||||
|
||||
with subtest("there is no Nix on the device"):
|
||||
machine.fail("command -v nix-env")
|
||||
|
||||
with subtest("/var is on the state partition and survives a reboot, the root does not"):
|
||||
src = machine.succeed("findmnt -n -o SOURCE /var").strip()
|
||||
assert "vda" in src, src
|
||||
machine.succeed("echo kept > /var/lib/marker; echo lost > /root/marker; sync")
|
||||
machine.shutdown()
|
||||
machine.start()
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
assert machine.succeed("cat /var/lib/marker").strip() == "kept"
|
||||
machine.fail("test -e /root/marker")
|
||||
machine.wait_for_unit("nftables.service")
|
||||
'';
|
||||
}
|
||||
Reference in New Issue
Block a user