nixos/clevis: init

Co-Authored-By: Julien Malka <julien@malka.sh>
This commit is contained in:
Camille Mondon 2023-11-18 19:37:56 +00:00 committed by Julien Malka
parent bea9ec6d4a
commit 27493b4d49
9 changed files with 255 additions and 5 deletions

View File

@ -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. -->

View File

@ -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

View File

@ -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
```
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.

View File

@ -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");
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.'';
})
cfg.devices));
warnings =
if cfg.useTang && !config.boot.initrd.network.enable && !config.boot.initrd.systemd.network.enable
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"
];
};
};
};
}

View File

@ -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;
@ -594,7 +596,7 @@ in
'';
type = with types; attrsOf (submodule (
{ name, ... }: { options = {
{ config, name, ... }: { options = {
name = mkOption {
visible = false;
@ -894,6 +896,19 @@ in
'';
};
};
config = mkIf (clevis.enable && (hasAttr name clevis.devices)) {
preOpenCommands = mkIf (!systemd.enable) ''
mkdir -p /clevis-${name}
mount -t ramfs none /clevis-${name}
clevis decrypt < /etc/clevis/${name}.jwe > /clevis-${name}/decrypted
'';
keyFile = "/clevis-${name}/decrypted";
fallbackToPassword = !systemd.enable;
postOpenCommands = mkIf (!systemd.enable) ''
umount /clevis-${name}
'';
};
}));
};
@ -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
mkIf (clevis.enable && systemd.enable) (
(mapAttrs'
(name: _: nameValuePair "cryptsetup-clevis-${name}" {
wantedBy = [ "systemd-cryptsetup@${utils.escapeSystemdPath name}.service" ];
before = [
"systemd-cryptsetup@${utils.escapeSystemdPath name}.service"
"initrd-switch-root.target"
"shutdown.target"
];
wants = [ "systemd-udev-settle.service" ] ++ optional clevis.useTang "network-online.target";
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}
umask 277
clevis decrypt < /etc/clevis/${name}.jwe > /clevis-${name}/decrypted
'';
conflicts = [ "initrd-switch-root.target" "shutdown.target" ];
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 ];
};
}

View File

@ -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}
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}
'';

View File

@ -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);
inInitrd = any (fs: fs == "zfs") config.boot.initrd.supportedFilesystems;
inSystem = any (fs: fs == "zfs") config.boot.supportedFilesystems;
@ -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";
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 = {
@ -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
${concatMapStringsSep "\n" (elem: "clevis decrypt < /etc/clevis/${elem}.jwe | zfs load-key ${elem} || true ") (filter (p: (elemAt (splitString "/" p) 0) == pool) clevisDatasets)}
${optionalString keyLocations.hasKeys ''
${keyLocations.command} | while IFS=$'\t' read ds kl ks; do
{
@ -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)}
${if isBool cfgZfs.requestEncryptionCredentials
then optionalString cfgZfs.requestEncryptionCredentials ''
zfs load-key -a

View File

@ -16,6 +16,7 @@
, ninja
, pkg-config
, tpm2-tools
, nixosTests
}:
stdenv.mkDerivation rec {
@ -29,6 +30,12 @@ stdenv.mkDerivation rec {
hash = "sha256-3J3ti/jRiv+p3eVvJD7u0ko28rPd8Gte0mCJaVaqyOs=";
};
patches = [
# Replaces the clevis-decrypt 300s timeout to a 10s timeout
# https://github.com/latchset/clevis/issues/289
./tang-timeout.patch
];
postPatch = ''
for f in $(find src/ -type f); do
grep -q "/bin/cat" "$f" && substituteInPlace "$f" \
@ -65,6 +72,14 @@ stdenv.mkDerivation rec {
"man"
];
passthru.tests = {
inherit (nixosTests.installer) clevisBcachefs clevisBcachefsFallback clevisLuks clevisLuksFallback clevisZfs clevisZfsFallback;
clevisLuksSystemdStage1 = nixosTests.installer-systemd-stage-1.clevisLuks;
clevisLuksFallbackSystemdStage1 = nixosTests.installer-systemd-stage-1.clevisLuksFallback;
clevisZfsSystemdStage1 = nixosTests.installer-systemd-stage-1.clevisZfs;
clevisZfsFallbackSystemdStage1 = nixosTests.installer-systemd-stage-1.clevisZfsFallback;
};
meta = with lib; {
description = "Automated Encryption Framework";
homepage = "https://github.com/latchset/clevis";

View File

@ -0,0 +1,13 @@
diff --git a/src/pins/tang/clevis-decrypt-tang b/src/pins/tang/clevis-decrypt-tang
index 72393b4..40b660f 100755
--- a/src/pins/tang/clevis-decrypt-tang
+++ b/src/pins/tang/clevis-decrypt-tang
@@ -101,7 +101,7 @@ xfr="$(jose jwk exc -i '{"alg":"ECMR"}' -l- -r- <<< "$clt$eph")"
rec_url="$url/rec/$kid"
ct="Content-Type: application/jwk+json"
-if ! rep="$(curl -sfg -X POST -H "$ct" --data-binary @- "$rec_url" <<< "$xfr")"; then
+if ! rep="$(curl --connect-timeout 10 -sfg -X POST -H "$ct" --data-binary @- "$rec_url" <<< "$xfr")"; then
echo "Error communicating with server $url" >&2
exit 1
fi