nixos/tzpfms: init (#517991)
This commit is contained in:
@@ -1961,6 +1961,7 @@
|
||||
./system/boot/systemd/userdbd.nix
|
||||
./system/boot/timesyncd.nix
|
||||
./system/boot/tmp.nix
|
||||
./system/boot/tzpfms.nix
|
||||
./system/boot/uki.nix
|
||||
./system/boot/unl0kr.nix
|
||||
./system/boot/uvesafb.nix
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
utils,
|
||||
...
|
||||
}:
|
||||
|
||||
# NOTE: the loading of keys is done in separate tzpfms systemd services
|
||||
# defined below rather than inline in the ZFS import scripts.
|
||||
|
||||
let
|
||||
cfgZFS = config.boot.zfs;
|
||||
cfg = cfgZFS.tzpfms;
|
||||
|
||||
datasetToPool = x: lib.elemAt (lib.splitString "/" x) 0;
|
||||
|
||||
pools = lib.unique (map datasetToPool cfg.datasets);
|
||||
|
||||
# All ZFS filesystems
|
||||
zfsFilesystems = lib.filter (x: x.fsType == "zfs") config.system.build.fileSystems;
|
||||
|
||||
# Pools that are already imported in initrd (have neededForBoot filesystems)
|
||||
fsToPool = fs: lib.elemAt (lib.splitString "/" fs.device) 0;
|
||||
rootPools = lib.unique (map fsToPool (lib.filter utils.fsNeededForBoot zfsFilesystems));
|
||||
|
||||
# Only include initrd resources if datasets belong to pools that need initrd import.
|
||||
# A pool needs initrd import if it has neededForBoot filesystems.
|
||||
initrdPools = lib.filter (pool: lib.elem pool rootPools) pools;
|
||||
systemPools = lib.filter (pool: !(lib.elem pool rootPools)) pools;
|
||||
|
||||
needsInitrd = initrdPools != [ ];
|
||||
|
||||
datasetsByPool = lib.groupBy datasetToPool cfg.datasets;
|
||||
|
||||
# Goup neededForBoot filesystems by pool → initrd mount units
|
||||
initrdMountsByPool = lib.foldl' (
|
||||
acc: fs:
|
||||
let
|
||||
p = lib.elemAt (lib.splitString "/" fs.device) 0;
|
||||
mount = "${utils.escapeSystemdPath ("/sysroot" + (lib.removeSuffix "/" fs.mountPoint))}.mount";
|
||||
in
|
||||
if utils.fsNeededForBoot fs then acc // { ${p} = (acc.${p} or [ ]) ++ [ mount ]; } else acc
|
||||
) { } zfsFilesystems;
|
||||
|
||||
# Group all ZFS filesystems by pool → system mount units
|
||||
systemMountsByPool = lib.foldl' (
|
||||
acc: fs:
|
||||
let
|
||||
pool = lib.elemAt (lib.splitString "/" fs.device) 0;
|
||||
mount = "${utils.escapeSystemdPath (lib.removeSuffix "/" fs.mountPoint)}.mount";
|
||||
in
|
||||
acc // { ${pool} = (acc.${pool} or [ ]) ++ [ mount ]; }
|
||||
) { } zfsFilesystems;
|
||||
|
||||
# Generate tzpfms key-loading bash script
|
||||
mkTzpfmsScript = datasets: /* bash */ ''
|
||||
tzpfms_load_key() {
|
||||
zfs-tpm-list -H ${backendArgs} "$@" 2>/dev/null | while IFS=$'\t' read -r name backend status _; do
|
||||
case "$backend" in
|
||||
${lib.optionalString (lib.elem "TPM2" cfg.backends) /* bash */ ''
|
||||
TPM2)
|
||||
zfs-tpm2-load-key "$name" || true
|
||||
;;
|
||||
''}
|
||||
${lib.optionalString (lib.elem "TPM1.X" cfg.backends) /* bash */ ''
|
||||
TPM1.X)
|
||||
zfs-tpm1x-load-key "$name" || true
|
||||
;;
|
||||
''}
|
||||
*)
|
||||
echo "[WARN] boot.zfs.tzpfms: Unsupported tzpfms backend: “$backend”; “$name” not unlocked" >&2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
${lib.concatMapStringsSep "\n" (ds: "tzpfms_load_key -u ${lib.escapeShellArg ds}") datasets}
|
||||
'';
|
||||
|
||||
mkTzpfmsService =
|
||||
{
|
||||
pool,
|
||||
mountUnits,
|
||||
script,
|
||||
}:
|
||||
{
|
||||
description = "Load TPM keys for ZFS pool “${pool}”";
|
||||
after = [ "zfs-import-${pool}.service" ];
|
||||
before = mountUnits ++ [ "zfs-import.target" ];
|
||||
requiredBy = mountUnits ++ [ "zfs-import.target" ];
|
||||
unitConfig.DefaultDependencies = "no";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
inherit script;
|
||||
};
|
||||
|
||||
backendArgs = lib.escapeShellArgs (
|
||||
lib.concatMap (b: [
|
||||
"-b"
|
||||
b
|
||||
]) cfg.backends
|
||||
);
|
||||
in
|
||||
{
|
||||
meta.maintainers = with lib.maintainers; [ toastal ];
|
||||
|
||||
options = {
|
||||
boot.zfs.tzpfms = {
|
||||
enable = lib.mkEnableOption ''
|
||||
TPM-backed ZFS encryption using tzpfms.
|
||||
Supports both TPM 2.0 & TPM 1.x.
|
||||
'';
|
||||
|
||||
package = lib.mkPackageOption pkgs "tzpfms" { };
|
||||
|
||||
backends = lib.mkOption {
|
||||
type =
|
||||
with lib.types;
|
||||
nonEmptyListOf (enum [
|
||||
"TPM2"
|
||||
"TPM1.X"
|
||||
]);
|
||||
default = [
|
||||
"TPM2"
|
||||
];
|
||||
description = ''
|
||||
TPM backends to include in for tzpfms.
|
||||
'';
|
||||
};
|
||||
|
||||
datasets = lib.mkOption {
|
||||
# Needs to be explicit so we can build thy systemd services
|
||||
type = with lib.types; nonEmptyListOf str;
|
||||
example = [
|
||||
"tank/root"
|
||||
"tank/var"
|
||||
];
|
||||
description = ''
|
||||
Explicit list of ZFS datasets to unlock with TPM at boot.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion =
|
||||
config.boot.supportedFilesystems.zfs or config.boot.initrd.supportedFilesystems.zfs or false;
|
||||
message = "ZFS filesystem support needs to be enabled for boot.tzpfms to work";
|
||||
}
|
||||
{
|
||||
assertion = initrdPools != { } -> config.boot.initrd.systemd.enable;
|
||||
message = "boot.zfs.tzpfms requires boot.initrd.systemd.enable = true";
|
||||
}
|
||||
{
|
||||
assertion =
|
||||
!(cfgZFS.requestEncryptionCredentials == true) || cfgZFS.requestEncryptionCredentials == [ ];
|
||||
message = ''
|
||||
boot.zfs.requestEncryptionCredentials = true would prompt for all
|
||||
encrypted dataset passphrases at boot, which conflicts with automatic
|
||||
TPM unlock via tzpfms. Either set it to false, or explicitly list the
|
||||
datasets that still need passphrase prompting.
|
||||
'';
|
||||
}
|
||||
(
|
||||
let
|
||||
intersected = lib.intersectLists cfg.datasets (
|
||||
if lib.isList cfgZFS.requestEncryptionCredentials then cfgZFS.requestEncryptionCredentials else [ ]
|
||||
);
|
||||
in
|
||||
{
|
||||
assertion = builtins.length intersected == 0;
|
||||
message = ''
|
||||
The following datasets are listed in both boot.zfs.tzpfms.datasets
|
||||
& boot.zfs.requestEncryptionCredentials, which would cause a
|
||||
passphrase prompt to block boot before tzpfms can unlock them via
|
||||
TPM:
|
||||
|
||||
${lib.concatMapStringsSep "\n" (d: "• ${d}") intersected}
|
||||
|
||||
Remove them from boot.zfs.requestEncryptionCredentials to allow
|
||||
automatic TPM unlock.
|
||||
'';
|
||||
}
|
||||
)
|
||||
];
|
||||
|
||||
environment.systemPackages = [ cfg.package ];
|
||||
|
||||
# Automatically register pools from tzpfms datasets as extraPools
|
||||
boot.zfs.extraPools = pools;
|
||||
|
||||
boot.initrd = lib.mkMerge [
|
||||
(lib.mkIf cfg.enable {
|
||||
availableKernelModules = [
|
||||
"tpm_tis"
|
||||
"tpm_crb"
|
||||
];
|
||||
})
|
||||
(lib.mkIf needsInitrd (
|
||||
lib.mkMerge [
|
||||
(lib.mkIf config.boot.initrd.systemd.enable {
|
||||
systemd.extraBin = {
|
||||
zfs-tpm-list = "${lib.getBin cfg.package}/bin/zfs-tpm-list";
|
||||
}
|
||||
// lib.optionalAttrs (lib.elem "TPM2" cfg.backends) {
|
||||
zfs-tpm2-load-key = "${lib.getBin cfg.package}/bin/zfs-tpm2-load-key";
|
||||
}
|
||||
// lib.optionalAttrs (lib.elem "TPM1.X" cfg.backends) {
|
||||
zfs-tpm1x-load-key = "${lib.getBin cfg.package}/bin/zfs-tpm1x-load-key";
|
||||
};
|
||||
systemd.storePaths =
|
||||
lib.optional (lib.elem "TPM2" cfg.backends) pkgs.tpm2-tss
|
||||
++ lib.optional (lib.elem "TPM1.X" cfg.backends) pkgs.trousers;
|
||||
systemd.services = lib.genAttrs' initrdPools (pool: {
|
||||
name = "tzpfms-load-${pool}";
|
||||
value = mkTzpfmsService {
|
||||
inherit pool;
|
||||
mountUnits = initrdMountsByPool.${pool} or [ ];
|
||||
script = mkTzpfmsScript (datasetsByPool.${pool} or [ ]);
|
||||
};
|
||||
});
|
||||
})
|
||||
]
|
||||
))
|
||||
];
|
||||
|
||||
systemd.services = lib.genAttrs' systemPools (
|
||||
pool:
|
||||
let
|
||||
mnts = systemMountsByPool.${pool} or [ ];
|
||||
in
|
||||
{
|
||||
name = "tzpfms-load-${pool}";
|
||||
value = mkTzpfmsService {
|
||||
inherit pool;
|
||||
mountUnits = mnts;
|
||||
script = mkTzpfmsScript (datasetsByPool.${pool} or [ ]);
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1740,6 +1740,7 @@ in
|
||||
twingate = runTest ./twingate.nix;
|
||||
txredisapi = runTest ./txredisapi.nix;
|
||||
typesense = runTest ./typesense.nix;
|
||||
tzpfms = runTest ./tzpfms.nix;
|
||||
tzupdate = runTest ./tzupdate.nix;
|
||||
ucarp = runTest ./ucarp.nix;
|
||||
udisks2 = runTest ./udisks2.nix;
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
{ lib, ... }:
|
||||
|
||||
{
|
||||
name = "tzpfms";
|
||||
|
||||
meta = {
|
||||
maintainers = with lib.maintainers; [ toastal ];
|
||||
};
|
||||
|
||||
nodes.machine =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
environment.systemPackages = [
|
||||
pkgs.jq
|
||||
pkgs.parted
|
||||
pkgs.tzpfms
|
||||
];
|
||||
|
||||
boot = {
|
||||
initrd.systemd.enable = true;
|
||||
loader = {
|
||||
systemd-boot.enable = true;
|
||||
timeout = 0;
|
||||
efi.canTouchEfiVariables = true;
|
||||
};
|
||||
supportedFilesystems = [ "zfs" ];
|
||||
zfs = {
|
||||
devNodes = "/dev";
|
||||
forceImportRoot = lib.mkDefault false;
|
||||
requestEncryptionCredentials = false;
|
||||
};
|
||||
};
|
||||
|
||||
networking.hostId = "deadbeef";
|
||||
|
||||
virtualisation = {
|
||||
emptyDiskImages = [ 1024 ];
|
||||
mountHostNixStore = true;
|
||||
useBootLoader = true;
|
||||
useEFIBoot = true;
|
||||
tpm.enable = true;
|
||||
};
|
||||
|
||||
specialisation.tzpfms-unlock.configuration = {
|
||||
boot = {
|
||||
kernelParams = [
|
||||
"rd.debug"
|
||||
"rd.log=all"
|
||||
];
|
||||
zfs = {
|
||||
devNodes = "/dev";
|
||||
requestEncryptionCredentials = false;
|
||||
tzpfms = {
|
||||
enable = true;
|
||||
datasets = [
|
||||
"tpmpool/boot"
|
||||
"tpmpool/data"
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
virtualisation.fileSystems = {
|
||||
"/bootz" = {
|
||||
device = "tpmpool/boot";
|
||||
fsType = "zfs";
|
||||
options = [ "zfsutil" ];
|
||||
neededForBoot = true;
|
||||
};
|
||||
"/dataz" = {
|
||||
device = "tpmpool/data";
|
||||
fsType = "zfs";
|
||||
options = [ "zfsutil" ];
|
||||
neededForBoot = false;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = /* python */ ''
|
||||
datasets = ["boot", "data"]
|
||||
|
||||
machine.start(allow_reboot=True)
|
||||
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
|
||||
machine.succeed("test -e /dev/tpm0")
|
||||
machine.succeed("test -e /dev/tpmrm0")
|
||||
|
||||
machine.succeed("parted --script /dev/vdb mklabel gpt")
|
||||
machine.succeed("parted --script /dev/vdb -- mkpart primary 1M 100%")
|
||||
|
||||
with subtest("Create encrypted ZFS datasets"):
|
||||
machine.succeed("zpool create -O mountpoint=none tpmpool /dev/vdb1")
|
||||
for ds in datasets:
|
||||
machine.succeed("echo aoeuhtns | zfs create -o encryption=aes-128-gcm -o keyformat=passphrase -o mountpoint=/" + ds + "z tpmpool/" + ds)
|
||||
|
||||
with subtest("Wrap keys to TPM with backup"):
|
||||
for ds in datasets:
|
||||
machine.succeed("printf '\\n\\n' | zfs-tpm2-change-key -b /tmp/tzpfms-backup-" + ds + ".key tpmpool/" + ds)
|
||||
machine.succeed("test -f /tmp/tzpfms-backup-" + ds + ".key")
|
||||
list = machine.succeed("zfs-tpm-list -H")
|
||||
for ds in datasets:
|
||||
assert "tpmpool/" + ds in list
|
||||
|
||||
with subtest("Verify backup keys work"):
|
||||
for ds in datasets:
|
||||
machine.succeed("zfs unmount tpmpool/" + ds + " || true")
|
||||
machine.succeed("zfs unload-key tpmpool/" + ds)
|
||||
machine.succeed("zfs load-key tpmpool/" + ds + " </tmp/tzpfms-backup-" + ds + ".key")
|
||||
assert "available" in machine.succeed("zfs get -Ho value keystatus tpmpool/" + ds)
|
||||
|
||||
with subtest("Verify loading key"):
|
||||
# Assertion that the key is loadable, as suggested in the manpage
|
||||
for ds in datasets:
|
||||
assert "OK" in machine.succeed("zfs-tpm2-load-key -n tpmpool/" + ds)
|
||||
|
||||
with subtest("Switch to tzpfms configuration & reboot"):
|
||||
machine.succeed("zpool export tpmpool || true")
|
||||
# Set the specialization as the default boot entry
|
||||
entry_id = machine.succeed("bootctl list --json=short | jq -r '.[] | select(.title | test(\"tzpfms-unlock\")) | .id'")
|
||||
assert id != "", "Missing boot entry"
|
||||
machine.succeed("bootctl set-default " + entry_id)
|
||||
machine.succeed("sync")
|
||||
machine.reboot()
|
||||
|
||||
with subtest("Verify automatic TPM unlock at boot"):
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
|
||||
for ds in datasets:
|
||||
status = machine.succeed("zfs get -Ho value keystatus tpmpool/" + ds).strip()
|
||||
print("tpmpool/" + ds + ": " + status)
|
||||
assert status == "available", ds + " key must be auto-loaded"
|
||||
'';
|
||||
}
|
||||
Reference in New Issue
Block a user