Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nixos/clevis: init & support for zfs, bcachefs and luks #257525

Merged
merged 2 commits into from
Dec 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions nixos/doc/manual/release-notes/rl-2405.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ In addition to numerous new and upgraded packages, this release has the followin

- [Anki Sync Server](https://docs.ankiweb.net/sync-server.html), the official sync server built into recent versions of Anki. Available as [services.anki-sync-server](#opt-services.anki-sync-server.enable).

- [Clevis](https://github.com/latchset/clevis), a pluggable framework for automated decryption, used to unlock encrypted devices in initrd. Available as [boot.initrd.clevis.enable](#opt-boot.initrd.clevis.enable).

## Backward Incompatibilities {#sec-release-24.05-incompatibilities}

<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
Expand Down
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -1423,6 +1423,7 @@
./system/activation/bootspec.nix
./system/activation/top-level.nix
./system/boot/binfmt.nix
./system/boot/clevis.nix
./system/boot/emergency-mode.nix
./system/boot/grow-partition.nix
./system/boot/initrd-network.nix
Expand Down
51 changes: 51 additions & 0 deletions nixos/modules/system/boot/clevis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Clevis {#module-boot-clevis}

[Clevis](https://github.com/latchset/clevis)
is a framework for automated decryption of resources.
Clevis allows for secure unattended disk decryption during boot, using decryption policies that must be satisfied for the data to decrypt.


## Create a JWE file containing your secret {#module-boot-clevis-create-secret}

The first step is to embed your secret in a [JWE](https://en.wikipedia.org/wiki/JSON_Web_Encryption) file.
JWE files have to be created through the clevis command line. 3 types of policies are supported:

1) TPM policies

Secrets are pinned against the presence of a TPM2 device, for example:
```
echo hi | clevis encrypt tpm2 '{}' > hi.jwe
JulienMalka marked this conversation as resolved.
Show resolved Hide resolved
```
2) Tang policies

Secrets are pinned against the presence of a Tang server, for example:
```
echo hi | clevis encrypt tang '{"url": "http://tang.local"}' > hi.jwe
```

3) Shamir Secret Sharing

Using Shamir's Secret Sharing ([sss](https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing)), secrets are pinned using a combination of the two preceding policies. For example:
```
echo hi | clevis encrypt sss \
'{"t": 2, "pins": {"tpm2": {"pcr_ids": "0"}, "tang": {"url": "http://tang.local"}}}' \
> hi.jwe
```

For more complete documentation on how to generate a secret with clevis, see the [clevis documentation](https://github.com/latchset/clevis).


## Activate unattended decryption of a resource at boot {#module-boot-clevis-activate}

In order to activate unattended decryption of a resource at boot, enable the `clevis` module:

```
boot.initrd.clevis.enable = true;
```

Then, specify the device you want to decrypt using a given clevis secret. Clevis will automatically try to decrypt the device at boot and will fallback to interactive unlocking if the decryption policy is not fulfilled.
```
boot.initrd.clevis.devices."/dev/nvme0n1p1".secretFile = ./nvme0n1p1.jwe;
```

Only `bcachefs`, `zfs` and `luks` encrypted devices are supported at this time.
107 changes: 107 additions & 0 deletions nixos/modules/system/boot/clevis.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
{ config, lib, pkgs, ... }:

with lib;

let
cfg = config.boot.initrd.clevis;
systemd = config.boot.initrd.systemd;
supportedFs = [ "zfs" "bcachefs" ];
in
{
meta.maintainers = with maintainers; [ julienmalka camillemndn ];
meta.doc = ./clevis.md;

options = {
boot.initrd.clevis.enable = mkEnableOption (lib.mdDoc "Clevis in initrd");

Copy link
Member

@RaitoBezarius RaitoBezarius Nov 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: too many linebreaks here.


boot.initrd.clevis.package = mkOption {
type = types.package;
default = pkgs.clevis;
defaultText = "pkgs.clevis";
description = lib.mdDoc "Clevis package";
};

boot.initrd.clevis.devices = mkOption {
description = "Encrypted devices that need to be unlocked at boot using Clevis";
default = { };
type = types.attrsOf (types.submodule ({
options.secretFile = mkOption {
description = lib.mdDoc "Clevis JWE file used to decrypt the device at boot, in concert with the chosen pin (one of TPM2, Tang server, or SSS).";
type = types.path;
};
}));
};

boot.initrd.clevis.useTang = mkOption {
description = "Whether the Clevis JWE file used to decrypt the devices uses a Tang server as a pin.";
default = false;
type = types.bool;
};

};

config = mkIf cfg.enable {

# Implementation of clevis unlocking for the supported filesystems are located directly in the respective modules.


assertions = (attrValues (mapAttrs
(device: _: {
assertion = (any (fs: fs.device == device && (elem fs.fsType supportedFs)) config.system.build.fileSystems) || (hasAttr device config.boot.initrd.luks.devices);
message = ''
No filesystem or LUKS device with the name ${device} is declared in your configuration.'';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this should say also that the device may not have the supported FS: zfs or bcachefs.

})
cfg.devices));


warnings =
if cfg.useTang && !config.boot.initrd.network.enable && !config.boot.initrd.systemd.network.enable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: that's exactly what lib.optional give you.
nit: cfg.useTag -> !(cfg.boot.initrd.network.enable || cfg.boot.initrd.systemd.network.enable) is more readable to me but I may be wrong in the conditional.

then [ "In order to use a Tang pinned secret you must configure networking in initrd" ]
else [ ];

boot.initrd = {
extraUtilsCommands = mkIf (!systemd.enable) ''
copy_bin_and_libs ${pkgs.jose}/bin/jose
copy_bin_and_libs ${pkgs.curl}/bin/curl
copy_bin_and_libs ${pkgs.bash}/bin/bash

copy_bin_and_libs ${pkgs.tpm2-tools}/bin/.tpm2-wrapped
mv $out/bin/{.tpm2-wrapped,tpm2}
cp {${pkgs.tpm2-tss},$out}/lib/libtss2-tcti-device.so.0

copy_bin_and_libs ${cfg.package}/bin/.clevis-wrapped
mv $out/bin/{.clevis-wrapped,clevis}

for BIN in ${cfg.package}/bin/clevis-decrypt*; do
copy_bin_and_libs $BIN
done

for BIN in $out/bin/clevis{,-decrypt{,-null,-tang,-tpm2}}; do
sed -i $BIN -e 's,${pkgs.bash},,' -e 's,${pkgs.coreutils},,'
done

sed -i $out/bin/clevis-decrypt-tpm2 -e 's,tpm2_,tpm2 ,'
'';

secrets = lib.mapAttrs' (name: value: nameValuePair "/etc/clevis/${name}.jwe" value.secretFile) cfg.devices;

systemd = {
extraBin = mkIf systemd.enable {
clevis = "${cfg.package}/bin/clevis";
curl = "${pkgs.curl}/bin/curl";
};

storePaths = mkIf systemd.enable [
cfg.package
"${pkgs.jose}/bin/jose"
"${pkgs.curl}/bin/curl"
"${pkgs.tpm2-tools}/bin/tpm2_createprimary"
"${pkgs.tpm2-tools}/bin/tpm2_flushcontext"
"${pkgs.tpm2-tools}/bin/tpm2_load"
"${pkgs.tpm2-tools}/bin/tpm2_unseal"
];
};
};
};
}
48 changes: 46 additions & 2 deletions nixos/modules/system/boot/luksroot.nix
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
{ config, options, lib, pkgs, ... }:
{ config, options, lib, utils, pkgs, ... }:

with lib;

let
luks = config.boot.initrd.luks;
clevis = config.boot.initrd.clevis;
systemd = config.boot.initrd.systemd;
kernelPackages = config.boot.kernelPackages;
defaultPrio = (mkOptionDefault {}).priority;

Expand Down Expand Up @@ -594,7 +596,7 @@ in
'';

type = with types; attrsOf (submodule (
{ name, ... }: { options = {
{ config, name, ... }: { options = {

name = mkOption {
visible = false;
Expand Down Expand Up @@ -894,6 +896,19 @@ in
'';
};
};

config = mkIf (clevis.enable && (hasAttr name clevis.devices)) {
preOpenCommands = mkIf (!systemd.enable) ''
mkdir -p /clevis-${name}
JulienMalka marked this conversation as resolved.
Show resolved Hide resolved
mount -t ramfs none /clevis-${name}
JulienMalka marked this conversation as resolved.
Show resolved Hide resolved
clevis decrypt < /etc/clevis/${name}.jwe > /clevis-${name}/decrypted
'';
keyFile = "/clevis-${name}/decrypted";
fallbackToPassword = !systemd.enable;
postOpenCommands = mkIf (!systemd.enable) ''
umount /clevis-${name}
'';
};
}));
};

Expand Down Expand Up @@ -1081,6 +1096,35 @@ in
boot.initrd.preLVMCommands = mkIf (!config.boot.initrd.systemd.enable) (commonFunctions + preCommands + concatStrings (mapAttrsToList openCommand preLVM) + postCommands);
boot.initrd.postDeviceCommands = mkIf (!config.boot.initrd.systemd.enable) (commonFunctions + preCommands + concatStrings (mapAttrsToList openCommand postLVM) + postCommands);

boot.initrd.systemd.services = let devicesWithClevis = filterAttrs (device: _: (hasAttr device clevis.devices)) luks.devices; in
Copy link
Member

@RaitoBezarius RaitoBezarius Nov 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: indent the let binding on the next line otherwise this is unreadable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One way you could address @RaitoBezarius's comment would be to make a service per device, and wed it to the systemd.device showing up. I think that's pretty clean, and it's a 1:1 map to the code in nixos/modules/system/boot/luksroot.nix

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, I think I'll address these remarks in a follow-up PR.

mkIf (clevis.enable && systemd.enable) (
(mapAttrs'
(name: _: nameValuePair "cryptsetup-clevis-${name}" {
wantedBy = [ "systemd-cryptsetup@${utils.escapeSystemdPath name}.service" ];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the name could be factored out in a let-binding as you use it on the next line.

before = [
"systemd-cryptsetup@${utils.escapeSystemdPath name}.service"
"initrd-switch-root.target"
"shutdown.target"
];
wants = [ "systemd-udev-settle.service" ] ++ optional clevis.useTang "network-online.target";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

systemd-udev-settle is broken by design and should not be used, ever. See #73095 (comment).
I don't know what you're trying to depend on, but this is most likely not the correct way.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use that to wait for TPM2 to be available. Do you know of another synchronization point for that ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you know the exact device path (say under /dev/ or /sys) in advance you can add a dependency on a device unit (see systemd.device(5)). Otherwise you could listen for the specific udev events using udevadm monitor -s <subsystem>/<devicetype> in you script. There's an example here.

Copy link
Contributor

@rnhmjoj rnhmjoj Jan 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems TPM2 devices get linked to /dev/tpmrm, so you should probably add a Wants=dev-tpmrm0.device dependency.

I don't know if systemd handles TPMs devices by default, though. If not you'll also have to add a TAG+=systemd rule on those devices.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, look what was just commited to systemd: systemd/systemd@4e1f003

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, in the future, we should definitely change this to use tpm2.target. At the moment, I don't know if we can add dependencies on dev-tpmrm0.device directly, because that would cause boot to delay waiting for that device to timeout in the event that one doesn't exist.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tpm2 target does exactly this: it adds a dependecy on dev-tpmrm0.device, so it will time out without a TPM.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rnhmjoj But tpm2.target is not in the initial transaction by default. It only gets added by systemd-tpm2-generator if it detects that the TPM2 exists in /dev or according to the firmware (via efi variables). Units aren't supposed to have Wants=tpm2.target, only After=tpm2.target so that they're ordered properly when a TPM exists.

Early boot programs that intend to access the TPM2 device should hence order themselves after this target unit, but not pull it in.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see. Anyway, it seems that this service does require a TPM, so should it be if not a timeout?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rnhmjoj I don't think a TPM is required necessarily? I think the .jwe determines if a TPM is required.

after = [ "systemd-modules-load.service" "systemd-udev-settle.service" ] ++ optional clevis.useTang "network-online.target";
script = ''
mkdir -p /clevis-${name}
mount -t ramfs none /clevis-${name}
JulienMalka marked this conversation as resolved.
Show resolved Hide resolved
umask 277
clevis decrypt < /etc/clevis/${name}.jwe > /clevis-${name}/decrypted
'';
conflicts = [ "initrd-switch-root.target" "shutdown.target" ];
JulienMalka marked this conversation as resolved.
Show resolved Hide resolved
unitConfig.DefaultDependencies = "no";
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStop = "${config.boot.initrd.systemd.package.util-linux}/bin/umount /clevis-${name}";
};
})
devicesWithClevis)
);

environment.systemPackages = [ pkgs.cryptsetup ];
};
}
10 changes: 9 additions & 1 deletion nixos/modules/tasks/filesystems/bcachefs.nix
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,15 @@ let
# bcachefs does not support mounting devices with colons in the path, ergo we don't (see #49671)
firstDevice = fs: lib.head (lib.splitString ":" fs.device);

openCommand = name: fs: ''
openCommand = name: fs: if config.boot.initrd.clevis.enable && (lib.hasAttr (firstDevice fs) config.boot.initrd.clevis.devices) then ''
if clevis decrypt < /etc/clevis/${firstDevice fs}.jwe | bcachefs unlock ${firstDevice fs}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit/future: what is the design for multi-device unlocks via Clevis in case of degraded devices? e.g. one of the disk is missing but the other is here, how does that behave?

then
printf "unlocked ${name} using clevis\n"
else
printf "falling back to interactive unlocking...\n"
tryUnlock ${name} ${firstDevice fs}
fi
'' else ''
tryUnlock ${name} ${firstDevice fs}
'';

Expand Down
13 changes: 11 additions & 2 deletions nixos/modules/tasks/filesystems/zfs.nix
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ let
cfgZED = config.services.zfs.zed;

selectModulePackage = package: config.boot.kernelPackages.${package.kernelModuleAttribute};
clevisDatasets = map (e: e.device) (filter (e: (hasAttr e.device config.boot.initrd.clevis.devices) && e.fsType == "zfs" && (fsNeededForBoot e)) config.system.build.fileSystems);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is mishandling fileSystems with device = null (the default).

For example, I'm mounting /boot by label with

  fileSystems."/boot" =
    { label = "boot";
      fsType = "ext4";
    };

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think you are right



inInitrd = any (fs: fs == "zfs") config.boot.initrd.supportedFilesystems;
inSystem = any (fs: fs == "zfs") config.boot.supportedFilesystems;

Expand Down Expand Up @@ -120,12 +123,12 @@ let
# but don't *require* it, because mounts shouldn't be killed if it's stopped.
# In the future, hopefully someone will complete this:
# https://github.com/zfsonlinux/zfs/pull/4943
wants = [ "systemd-udev-settle.service" ];
wants = [ "systemd-udev-settle.service" ] ++ optional (config.boot.initrd.clevis.useTang) "network-online.target";
JulienMalka marked this conversation as resolved.
Show resolved Hide resolved
after = [
"systemd-udev-settle.service"
"systemd-modules-load.service"
"systemd-ask-password-console.service"
];
] ++ optional (config.boot.initrd.clevis.useTang) "network-online.target";
requiredBy = getPoolMounts prefix pool ++ [ "zfs-import.target" ];
before = getPoolMounts prefix pool ++ [ "zfs-import.target" ];
unitConfig = {
Expand Down Expand Up @@ -154,6 +157,9 @@ let
poolImported "${pool}" || poolImport "${pool}" # Try one last time, e.g. to import a degraded pool.
fi
if poolImported "${pool}"; then
Copy link
Contributor

@misuzu misuzu Jun 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the entire zfs pool is encrypted it will ask the password before this check and clevis won't be able to decrypt the pool.

Jun 11 22:42:05 localhost zfs-import-rpool-start[327]: importing ZFS pool "rpool"...
Jun 11 22:42:05 localhost zfs-import-rpool-start[652]: Key load error: Keys must be loaded for encryption root of 'rpool/safe/root' (rpool).

I have this config:

{ config, pkgs, ... }:
{
  boot.initrd.clevis.enable = true;
  boot.initrd.clevis.devices."rpool/safe/root".secretFile = "/etc/initrd/clevis/rpool.jwe";
}

The filesystems config is roughly like this:

{
  fileSystems."/" =
    { device = "rpool/safe/root";
      fsType = "zfs";
    };

  fileSystems."/nix" =
    { device = "rpool/local/nix";
      fsType = "zfs";
    };

  fileSystems."/boot" =
    { device = "/dev/disk/by-uuid/E08C-AFC5";
      fsType = "vfat";
      options = [ "fmask=0022" "dmask=0022" ];
    };
}
% zfs list -rHo name,keylocation,keystatus -t volume,filesystem rpool
rpool   prompt  available
rpool/local     none    available
rpool/local/nix none    available
rpool/reserved  none    available
rpool/safe      none    available
rpool/safe/root none    available

With this config it tries to execute zfs load-key rpool/safe/root which fails with Key load error: Keys must be loaded for encryption root of 'rpool/safe/root' , but it works if I execute zfs load-key rpool:

-bash-5.2# zfs load-key rpool/safe/root
Key load error: Keys must be loaded for encryption root of 'rpool/safe/root' (rpool).
-bash-5.2# zfs load-key rpool
Enter passphrase for 'rpool':

If I use boot.initrd.clevis.devices."rpool".secretFile = "/etc/initrd/clevis/rpool.jwe"; it fails to evaluate:

       Failed assertions:
       - No filesystem or LUKS device with the name rpool is declared in your configuration.

This looks like a module deficiency to me.

${concatMapStringsSep "\n" (elem: "clevis decrypt < /etc/clevis/${elem}.jwe | zfs load-key ${elem} || true ") (filter (p: (elemAt (splitString "/" p) 0) == pool) clevisDatasets)}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you better factor out the string logic for a single item in a mkClevisDecryptCommand so that we can distinguish things better.



${optionalString keyLocations.hasKeys ''
${keyLocations.command} | while IFS=$'\t' read ds kl ks; do
{
Expand Down Expand Up @@ -623,6 +629,9 @@ in
fi
poolImported "${pool}" || poolImport "${pool}" # Try one last time, e.g. to import a degraded pool.
fi

${concatMapStringsSep "\n" (elem: "clevis decrypt < /etc/clevis/${elem}.jwe | zfs load-key ${elem}") (filter (p: (elemAt (splitString "/" p) 0) == pool) clevisDatasets)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this add calls to clevis even when boot.initrd.clevis.enable is false?

I think that's why I started getting this evaluation error yesterday: https://gist.github.com/aij/bb5c49f0afbde2b3efdfc8f30c1e7b6e

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have clevis devices set but clevis disabled ? Or do you have nothing clevis related in your config but still evaluation errors ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for reporting that, I am proposing a fix here: #272061

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry about the delay. FWIW, I had nothing clevis related in my configs.

Fix looks good. Thanks!


${if isBool cfgZfs.requestEncryptionCredentials
then optionalString cfgZfs.requestEncryptionCredentials ''
zfs load-key -a
Expand Down
4 changes: 4 additions & 0 deletions nixos/tests/installer-systemd-stage-1.nix
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
stratisRoot
swraid
zfsroot
clevisLuks
clevisLuksFallback
clevisZfs
clevisZfsFallback
;

}
Loading