nixos/system/boot/tzpfms: init

This commit is contained in:
·𐑑𐑴𐑕𐑑𐑩𐑤
2026-04-24 20:35:35 +07:00
parent 102ddbb566
commit a7be344fe7
2 changed files with 248 additions and 0 deletions
+1
View File
@@ -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
+247
View File
@@ -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 [ ]);
};
}
);
};
}