diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix index 7fb5c2259062..7a606afcdd9d 100644 --- a/nixos/modules/system/boot/systemd.nix +++ b/nixos/modules/system/boot/systemd.nix @@ -213,6 +213,12 @@ let # Capsule support "capsule@.service" "capsule.slice" + + # Factory reset + "factory-reset.target" + "systemd-factory-reset-request.service" + "systemd-factory-reset-reboot.service" + "systemd-tpm2-clear.service" ] ++ cfg.additionalUpstreamSystemUnits; @@ -222,6 +228,7 @@ let "local-fs.target.wants" "multi-user.target.wants" "timers.target.wants" + "factory-reset.target.wants" ]; proxy_env = config.networking.proxy.envVars; diff --git a/nixos/modules/system/boot/systemd/initrd.nix b/nixos/modules/system/boot/systemd/initrd.nix index 53a73641f245..0c20432e58f4 100644 --- a/nixos/modules/system/boot/systemd/initrd.nix +++ b/nixos/modules/system/boot/systemd/initrd.nix @@ -65,6 +65,8 @@ let "syslog.socket" "systemd-ask-password-console.path" "systemd-ask-password-console.service" + "systemd-factory-reset-complete.service" + "factory-reset-now.target" "systemd-fsck@.service" "systemd-halt.service" "systemd-hibernate-resume.service" @@ -555,6 +557,7 @@ in "${cfg.package}/lib/systemd/systemd-sysctl" "${cfg.package}/lib/systemd/systemd-bsod" "${cfg.package}/lib/systemd/systemd-sysroot-fstab-check" + "${cfg.package}/lib/systemd/systemd-factory-reset" # generators "${cfg.package}/lib/systemd/system-generators/systemd-debug-generator" @@ -562,6 +565,7 @@ in "${cfg.package}/lib/systemd/system-generators/systemd-gpt-auto-generator" "${cfg.package}/lib/systemd/system-generators/systemd-hibernate-resume-generator" "${cfg.package}/lib/systemd/system-generators/systemd-run-generator" + "${cfg.package}/lib/systemd/system-generators/systemd-factory-reset-generator" # utilities needed by systemd "${cfg.package.util-linux}/bin/mount" diff --git a/nixos/tests/systemd-repart.nix b/nixos/tests/systemd-repart.nix index 96a33633dc46..01faf69d6a51 100644 --- a/nixos/tests/systemd-repart.nix +++ b/nixos/tests/systemd-repart.nix @@ -277,4 +277,106 @@ in assert "Adding new partition 2 to partition table." in systemd_repart_logs ''; }; + + factory-reset = makeTest { + name = "systemd-repart-factory-reset"; + meta.maintainers = with maintainers; [ willibutz ]; + + nodes.machine = + { pkgs, lib, ... }: + { + imports = [ common ]; + + virtualisation = { + useEFIBoot = true; + tpm.enable = true; + efi.OVMF = pkgs.OVMFFull; + useDefaultFilesystems = false; + fileSystems = { + "/state" = { + device = "/dev/mapper/state"; + fsType = "ext4"; + }; + }; + }; + + boot = { + loader.systemd-boot.enable = true; + + initrd = { + systemd = { + enable = true; + # avoids reaching cryptsetup.target before recreation of the + # "state" volume completed, during the factory reset + services.systemd-repart.before = [ + "systemd-cryptsetup@state.service" + ]; + repart = { + enable = true; + extraArgs = [ + "--tpm2-pcrs=platform-code" + ]; + }; + }; + luks.devices = lib.mkVMOverride { + state = { + device = "/dev/disk/by-partlabel/state"; + crypttabExtraOpts = [ "tpm2-device=auto" ]; + }; + }; + }; + }; + + systemd.repart.partitions = { + "10-esp".Type = "esp"; + "20-root".Type = "linux-generic"; + "30-state" = { + Type = "linux-generic"; + Label = "state"; + Format = "ext4"; + Encrypt = "tpm2"; + SizeMinBytes = "64M"; + SizeMaxBytes = "64M"; + FactoryReset = true; + }; + }; + + # doesn't actually reboot through the service because otherwise the test + # instrumentation becomes very unreliable, instead uses machine.reboot() + systemd.services.systemd-factory-reset-reboot.enable = false; + }; + + testScript = + { nodes, ... }: + # python + '' + ${useDiskImage { + inherit (nodes) machine; + sizeDiff = "+64M"; + }} + + machine.start(allow_reboot=True) + machine.wait_for_unit("default.target") + + first_uuid = machine.succeed("blkid -s UUID -o value /dev/disk/by-label/state") + + machine.succeed("mountpoint /state") + machine.succeed("touch /state/foo") + + with subtest("factory reset requested through target"): + machine.systemctl("start factory-reset.target") + + # reboot manually to keep control over test vm + machine.reboot() + machine.wait_for_unit("default.target") + + with subtest("state partition recreated and empty after reset"): + second_uuid = machine.succeed("blkid -s UUID -o value /dev/disk/by-label/state") + t.assertNotEqual(first_uuid, second_uuid) + + machine.succeed("mountpoint /state") + machine.fail("test -e /state/foo") + ''; + }; + }