add support for building fully dm-verity protected images with systemd-repart (#343252)
This commit is contained in:
commit
fed418aaf1
@ -46,6 +46,9 @@
|
||||
If you experience any issues, please report them.
|
||||
The original Perl script can still be used for now by setting `system.switch.enableNg` to `false`.
|
||||
|
||||
- Support for mounting filesystems from block devices protected with [dm-verity](https://docs.kernel.org/admin-guide/device-mapper/verity.html)
|
||||
was added through the `boot.initrd.systemd.dmVerity` option.
|
||||
|
||||
- The [Xen Hypervisor](https://xenproject.org) is once again available as a virtualisation option under [`virtualisation.xen`](#opt-virtualisation.xen.enable).
|
||||
- This release includes Xen [4.17.5](https://wiki.xenproject.org/wiki/Xen_Project_4.17_Release_Notes), [4.18.3](https://wiki.xenproject.org/wiki/Xen_Project_4.18_Release_Notes) and [4.19.0](https://wiki.xenproject.org/wiki/Xen_Project_4.19_Release_Notes), as well as support for booting the hypervisor on EFI systems.
|
||||
::: {.warning}
|
||||
|
78
nixos/modules/image/assert_uki_repart_match.py
Normal file
78
nixos/modules/image/assert_uki_repart_match.py
Normal file
@ -0,0 +1,78 @@
|
||||
import json
|
||||
import sys
|
||||
|
||||
store_verity_type = "@NIX_STORE_VERITY@" # replaced at import by Nix
|
||||
|
||||
|
||||
def extract_uki_cmdline_params(ukify_json: dict) -> dict[str, str]:
|
||||
"""
|
||||
Return a dict of the parameters in the .cmdline section of the UKI
|
||||
Exits early if "usrhash" is not included.
|
||||
"""
|
||||
cmdline = ukify_json.get(".cmdline", {}).get("text")
|
||||
if cmdline is None:
|
||||
print("Failed to get cmdline from ukify output")
|
||||
|
||||
params = {}
|
||||
for param in cmdline.split():
|
||||
key, val = param.partition("=")[::2]
|
||||
params[key] = val
|
||||
|
||||
if "usrhash" not in params:
|
||||
print(
|
||||
f"UKI cmdline does not contain a usrhash:\n{cmdline}"
|
||||
)
|
||||
exit(1)
|
||||
|
||||
return params
|
||||
|
||||
|
||||
def hashes_match(partition: dict[str, str], expected: str) -> bool:
|
||||
"""
|
||||
Checks if the value of the "roothash" key in the passed partition object matches `expected`.
|
||||
"""
|
||||
if partition.get("roothash") != expected:
|
||||
pretty_part = json.dumps(partition, indent=2)
|
||||
print(
|
||||
f"hash mismatch, expected to find roothash {expected} in:\n{pretty_part}"
|
||||
)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def check_partitions(
|
||||
partitions: list[dict], uki_params: dict[str, str]
|
||||
) -> bool:
|
||||
"""
|
||||
Checks if the usrhash from `uki_params` has a matching roothash
|
||||
for the corresponding partition in `partitions`.
|
||||
"""
|
||||
for part in partitions:
|
||||
if part.get("type") == store_verity_type:
|
||||
expected = uki_params["usrhash"]
|
||||
return hashes_match(part, expected)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ukify_json = json.load(sys.stdin)
|
||||
repart_json_output = sys.argv[1]
|
||||
|
||||
with open(repart_json_output, "r") as r:
|
||||
repart_json = json.load(r)
|
||||
|
||||
uki_params = extract_uki_cmdline_params(ukify_json)
|
||||
|
||||
if check_partitions(repart_json, uki_params):
|
||||
print("UKI and repart verity hashes match")
|
||||
else:
|
||||
print("Compatibility check for UKI and image failed!")
|
||||
print(f"UKI cmdline parameters:\n{uki_params}")
|
||||
print(f"repart config: {repart_json_output}")
|
||||
exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
209
nixos/modules/image/repart-verity-store.nix
Normal file
209
nixos/modules/image/repart-verity-store.nix
Normal file
@ -0,0 +1,209 @@
|
||||
# opinionated module that can be used to build nixos images with
|
||||
# a dm-verity protected nix store
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.image.repart.verityStore;
|
||||
|
||||
verityMatchKey = "store";
|
||||
|
||||
# TODO: make these and other arch mappings available from systemd-lib for example
|
||||
partitionTypes = {
|
||||
usr =
|
||||
{
|
||||
"x86_64" = "usr-x86-64";
|
||||
"arm64" = "usr-arm64";
|
||||
}
|
||||
."${pkgs.stdenv.hostPlatform.linuxArch}";
|
||||
|
||||
usr-verity =
|
||||
{
|
||||
"x86_64" = "usr-x86-64-verity";
|
||||
"arm64" = "usr-arm64-verity";
|
||||
}
|
||||
."${pkgs.stdenv.hostPlatform.linuxArch}";
|
||||
};
|
||||
|
||||
verityHashCheck =
|
||||
pkgs.buildPackages.writers.writePython3Bin "assert_uki_repart_match.py"
|
||||
{
|
||||
flakeIgnore = [ "E501" ]; # ignores PEP8's line length limit of 79 (black defaults to 88 characters)
|
||||
}
|
||||
(
|
||||
builtins.replaceStrings [ "@NIX_STORE_VERITY@" ] [
|
||||
partitionTypes.usr-verity
|
||||
] (builtins.readFile ./assert_uki_repart_match.py)
|
||||
);
|
||||
in
|
||||
{
|
||||
options.image.repart.verityStore = {
|
||||
enable = lib.mkEnableOption "building images with a dm-verity protected nix store";
|
||||
|
||||
ukiPath = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "/EFI/Linux/${config.system.boot.loader.ukiFile}";
|
||||
defaultText = "/EFI/Linux/\${config.system.boot.loader.ukiFile}";
|
||||
description = ''
|
||||
Specify the location on the ESP where the UKI is placed.
|
||||
'';
|
||||
};
|
||||
|
||||
partitionIds = {
|
||||
esp = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "00-esp";
|
||||
description = ''
|
||||
Specify the attribute name of the ESP.
|
||||
'';
|
||||
};
|
||||
store-verity = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "10-store-verity";
|
||||
description = ''
|
||||
Specify the attribute name of the store's dm-verity hash partition.
|
||||
'';
|
||||
};
|
||||
store = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "20-store";
|
||||
description = ''
|
||||
Specify the attribute name of the store partition.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
boot.initrd.systemd.dmVerity.enable = true;
|
||||
|
||||
image.repart.partitions = {
|
||||
# dm-verity hash partition
|
||||
${cfg.partitionIds.store-verity}.repartConfig = {
|
||||
Type = partitionTypes.usr-verity;
|
||||
Verity = "hash";
|
||||
VerityMatchKey = lib.mkDefault verityMatchKey;
|
||||
Label = lib.mkDefault "store-verity";
|
||||
};
|
||||
# dm-verity data partition that contains the nix store
|
||||
${cfg.partitionIds.store} = {
|
||||
storePaths = [ config.system.build.toplevel ];
|
||||
repartConfig = {
|
||||
Type = partitionTypes.usr;
|
||||
Verity = "data";
|
||||
Format = lib.mkDefault "erofs";
|
||||
VerityMatchKey = lib.mkDefault verityMatchKey;
|
||||
Label = lib.mkDefault "store";
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
system.build = {
|
||||
|
||||
# intermediate system image without ESP
|
||||
intermediateImage =
|
||||
(config.system.build.image.override {
|
||||
# always disable compression for the intermediate image
|
||||
compression.enable = false;
|
||||
}).overrideAttrs
|
||||
(
|
||||
_: previousAttrs: {
|
||||
# make it easier to identify the intermediate image in build logs
|
||||
pname = "${previousAttrs.pname}-intermediate";
|
||||
|
||||
# do not prepare the ESP, this is done in the final image
|
||||
systemdRepartFlags = previousAttrs.systemdRepartFlags ++ [ "--defer-partitions=esp" ];
|
||||
|
||||
# the image will be self-contained so we can drop references
|
||||
# to the closure that was used to build it
|
||||
unsafeDiscardReferences.out = true;
|
||||
}
|
||||
);
|
||||
|
||||
# UKI with embedded usrhash from intermediateImage
|
||||
uki =
|
||||
let
|
||||
inherit (config.system.boot.loader) ukiFile;
|
||||
cmdline = "init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams}";
|
||||
in
|
||||
# override the default UKI
|
||||
lib.mkOverride 99 (
|
||||
pkgs.runCommand ukiFile
|
||||
{
|
||||
nativeBuildInputs = [
|
||||
pkgs.jq
|
||||
pkgs.systemdUkify
|
||||
];
|
||||
}
|
||||
''
|
||||
mkdir -p $out
|
||||
|
||||
# Extract the usrhash from the output of the systemd-repart invocation for the intermediate image.
|
||||
usrhash=$(jq -r \
|
||||
'.[] | select(.type=="${partitionTypes.usr-verity}") | .roothash' \
|
||||
${config.system.build.intermediateImage}/repart-output.json
|
||||
)
|
||||
|
||||
# Build UKI with the embedded usrhash.
|
||||
ukify build \
|
||||
--config=${config.boot.uki.configFile} \
|
||||
--cmdline="${cmdline} usrhash=$usrhash" \
|
||||
--output="$out/${ukiFile}"
|
||||
''
|
||||
);
|
||||
|
||||
# final system image that is created from the intermediate image by injecting the UKI from above
|
||||
finalImage =
|
||||
(config.system.build.image.override {
|
||||
# continue building with existing intermediate image
|
||||
createEmpty = false;
|
||||
}).overrideAttrs
|
||||
(
|
||||
finalAttrs: previousAttrs:
|
||||
let
|
||||
copyUki = "CopyFiles=${config.system.build.uki}/${config.system.boot.loader.ukiFile}:${cfg.ukiPath}";
|
||||
in
|
||||
{
|
||||
nativeBuildInputs = previousAttrs.nativeBuildInputs ++ [
|
||||
pkgs.systemdUkify
|
||||
verityHashCheck
|
||||
];
|
||||
|
||||
postPatch = ''
|
||||
# add entry to inject UKI into ESP
|
||||
echo '${copyUki}' >> $finalRepartDefinitions/${cfg.partitionIds.esp}.conf
|
||||
'';
|
||||
|
||||
preBuild = ''
|
||||
# check that we build the final image with the same intermediate image for
|
||||
# which the injected UKI was built by comparing the UKI cmdline with the repart output
|
||||
# of the intermediate image
|
||||
#
|
||||
# This is necessary to notice incompatible substitutions of
|
||||
# non-reproducible store paths, for example when working with distributed
|
||||
# builds, or when offline-signing the UKI.
|
||||
ukify --json=short inspect ${config.system.build.uki}/${config.system.boot.loader.ukiFile} \
|
||||
| assert_uki_repart_match.py "${config.system.build.intermediateImage}/repart-output.json"
|
||||
|
||||
# copy the uncompressed intermediate image, so that systemd-repart picks it up
|
||||
cp -v ${config.system.build.intermediateImage}/${config.image.repart.imageFileBasename}.raw .
|
||||
chmod +w ${config.image.repart.imageFileBasename}.raw
|
||||
'';
|
||||
|
||||
# the image will be self-contained so we can drop references
|
||||
# to the closure that was used to build it
|
||||
unsafeDiscardReferences.out = true;
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
nikstur
|
||||
willibutz
|
||||
];
|
||||
}
|
@ -69,6 +69,10 @@ let
|
||||
}) opts;
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
./repart-verity-store.nix
|
||||
];
|
||||
|
||||
options.image.repart = {
|
||||
|
||||
name = lib.mkOption {
|
||||
|
@ -1625,6 +1625,7 @@
|
||||
./system/boot/stage-2.nix
|
||||
./system/boot/systemd.nix
|
||||
./system/boot/systemd/coredump.nix
|
||||
./system/boot/systemd/dm-verity.nix
|
||||
./system/boot/systemd/initrd-secrets.nix
|
||||
./system/boot/systemd/initrd.nix
|
||||
./system/boot/systemd/journald.nix
|
||||
|
61
nixos/modules/system/boot/systemd/dm-verity.nix
Normal file
61
nixos/modules/system/boot/systemd/dm-verity.nix
Normal file
@ -0,0 +1,61 @@
|
||||
{ config, lib, ... }:
|
||||
|
||||
let
|
||||
cfg = config.boot.initrd.systemd.dmVerity;
|
||||
in
|
||||
{
|
||||
options = {
|
||||
boot.initrd.systemd.dmVerity = {
|
||||
enable = lib.mkEnableOption "dm-verity" // {
|
||||
description = ''
|
||||
Mount verity-protected block devices in the initrd.
|
||||
|
||||
Enabling this option allows to use `systemd-veritysetup` and
|
||||
`systemd-veritysetup-generator` in the initrd.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = config.boot.initrd.systemd.enable;
|
||||
message = ''
|
||||
'boot.initrd.systemd.dmVerity.enable' requires 'boot.initrd.systemd.enable' to be enabled.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
boot.initrd = {
|
||||
availableKernelModules = [
|
||||
"dm_mod"
|
||||
"dm_verity"
|
||||
];
|
||||
|
||||
# dm-verity needs additional udev rules from LVM to work.
|
||||
services.lvm.enable = true;
|
||||
|
||||
# The additional targets and store paths allow users to integrate verity-protected devices
|
||||
# through the systemd tooling.
|
||||
systemd = {
|
||||
additionalUpstreamUnits = [
|
||||
"veritysetup-pre.target"
|
||||
"veritysetup.target"
|
||||
"remote-veritysetup.target"
|
||||
];
|
||||
|
||||
storePaths = [
|
||||
"${config.boot.initrd.systemd.package}/lib/systemd/systemd-veritysetup"
|
||||
"${config.boot.initrd.systemd.package}/lib/systemd/system-generators/systemd-veritysetup-generator"
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
msanft
|
||||
nikstur
|
||||
willibutz
|
||||
];
|
||||
}
|
@ -128,6 +128,7 @@ in {
|
||||
apcupsd = handleTest ./apcupsd.nix {};
|
||||
apfs = runTest ./apfs.nix;
|
||||
appliance-repart-image = runTest ./appliance-repart-image.nix;
|
||||
appliance-repart-image-verity-store = runTest ./appliance-repart-image-verity-store.nix;
|
||||
apparmor = handleTest ./apparmor.nix {};
|
||||
archi = handleTest ./archi.nix {};
|
||||
aria2 = handleTest ./aria2.nix {};
|
||||
|
130
nixos/tests/appliance-repart-image-verity-store.nix
Normal file
130
nixos/tests/appliance-repart-image-verity-store.nix
Normal file
@ -0,0 +1,130 @@
|
||||
# similar to the appliance-repart-image test but with a dm-verity
|
||||
# protected nix store and tmpfs as rootfs
|
||||
{ lib, ... }:
|
||||
|
||||
{
|
||||
name = "appliance-repart-image-verity-store";
|
||||
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
nikstur
|
||||
willibutz
|
||||
];
|
||||
|
||||
nodes.machine =
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (config.image.repart.verityStore) partitionIds;
|
||||
in
|
||||
{
|
||||
imports = [ ../modules/image/repart.nix ];
|
||||
|
||||
virtualisation.fileSystems = lib.mkVMOverride {
|
||||
"/" = {
|
||||
fsType = "tmpfs";
|
||||
options = [ "mode=0755" ];
|
||||
};
|
||||
|
||||
"/usr" = {
|
||||
device = "/dev/mapper/usr";
|
||||
# explicitly mount it read-only otherwise systemd-remount-fs will fail
|
||||
options = [ "ro" ];
|
||||
fsType = config.image.repart.partitions.${partitionIds.store}.repartConfig.Format;
|
||||
};
|
||||
|
||||
# bind-mount the store
|
||||
"/nix/store" = {
|
||||
device = "/usr/nix/store";
|
||||
options = [ "bind" ];
|
||||
};
|
||||
};
|
||||
|
||||
image.repart = {
|
||||
verityStore = {
|
||||
enable = true;
|
||||
# by default the module works with systemd-boot, for simplicity this test directly boots the UKI
|
||||
ukiPath = "/EFI/BOOT/BOOT${lib.toUpper config.nixpkgs.hostPlatform.efiArch}.EFI";
|
||||
};
|
||||
|
||||
name = "appliance-verity-store-image";
|
||||
|
||||
partitions = {
|
||||
${partitionIds.esp} = {
|
||||
# the UKI is injected into this partition by the verityStore module
|
||||
repartConfig = {
|
||||
Type = "esp";
|
||||
Format = "vfat";
|
||||
SizeMinBytes = if config.nixpkgs.hostPlatform.isx86_64 then "64M" else "96M";
|
||||
};
|
||||
};
|
||||
${partitionIds.store-verity}.repartConfig = {
|
||||
Minimize = "best";
|
||||
};
|
||||
${partitionIds.store}.repartConfig = {
|
||||
Minimize = "best";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
virtualisation = {
|
||||
directBoot.enable = false;
|
||||
mountHostNixStore = false;
|
||||
useEFIBoot = true;
|
||||
};
|
||||
|
||||
boot = {
|
||||
loader.grub.enable = false;
|
||||
initrd.systemd.enable = true;
|
||||
};
|
||||
|
||||
system.image = {
|
||||
id = "nixos-appliance";
|
||||
version = "1";
|
||||
};
|
||||
|
||||
# don't create /usr/bin/env
|
||||
# this would require some extra work on read-only /usr
|
||||
# and it is not a strict necessity
|
||||
system.activationScripts.usrbinenv = lib.mkForce "";
|
||||
};
|
||||
|
||||
testScript =
|
||||
{ nodes, ... }: # python
|
||||
''
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
tmp_disk_image = tempfile.NamedTemporaryFile()
|
||||
|
||||
subprocess.run([
|
||||
"${nodes.machine.virtualisation.qemu.package}/bin/qemu-img",
|
||||
"create",
|
||||
"-f",
|
||||
"qcow2",
|
||||
"-b",
|
||||
"${nodes.machine.system.build.finalImage}/${nodes.machine.image.repart.imageFile}",
|
||||
"-F",
|
||||
"raw",
|
||||
tmp_disk_image.name,
|
||||
])
|
||||
|
||||
os.environ['NIX_DISK_IMAGE'] = tmp_disk_image.name
|
||||
|
||||
machine.wait_for_unit("default.target")
|
||||
|
||||
with subtest("Running with volatile root"):
|
||||
machine.succeed("findmnt --kernel --type tmpfs /")
|
||||
|
||||
with subtest("/nix/store is backed by dm-verity protected fs"):
|
||||
verity_info = machine.succeed("dmsetup info --target verity usr")
|
||||
assert "ACTIVE" in verity_info,f"unexpected verity info: {verity_info}"
|
||||
|
||||
backing_device = machine.succeed("df --output=source /nix/store | tail -n1").strip()
|
||||
assert "/dev/mapper/usr" == backing_device,"unexpected backing device: {backing_device}"
|
||||
'';
|
||||
}
|
Loading…
Reference in New Issue
Block a user