From 27493b4d49b6d92f4baa049424cbb2fa48b4c948 Mon Sep 17 00:00:00 2001 From: Camille Mondon Date: Sat, 18 Nov 2023 19:37:56 +0000 Subject: [PATCH] nixos/clevis: init Co-Authored-By: Julien Malka --- .../manual/release-notes/rl-2405.section.md | 2 + nixos/modules/module-list.nix | 1 + nixos/modules/system/boot/clevis.md | 51 +++++++++ nixos/modules/system/boot/clevis.nix | 107 ++++++++++++++++++ nixos/modules/system/boot/luksroot.nix | 48 +++++++- nixos/modules/tasks/filesystems/bcachefs.nix | 10 +- nixos/modules/tasks/filesystems/zfs.nix | 13 ++- pkgs/tools/security/clevis/default.nix | 15 +++ pkgs/tools/security/clevis/tang-timeout.patch | 13 +++ 9 files changed, 255 insertions(+), 5 deletions(-) create mode 100644 nixos/modules/system/boot/clevis.md create mode 100644 nixos/modules/system/boot/clevis.nix create mode 100644 pkgs/tools/security/clevis/tang-timeout.patch diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md index ba4d26518db8..729b52e09a16 100644 --- a/nixos/doc/manual/release-notes/rl-2405.section.md +++ b/nixos/doc/manual/release-notes/rl-2405.section.md @@ -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} diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 7f708f6e57c7..bf4e51be8578 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -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 diff --git a/nixos/modules/system/boot/clevis.md b/nixos/modules/system/boot/clevis.md new file mode 100644 index 000000000000..91eb728a919e --- /dev/null +++ b/nixos/modules/system/boot/clevis.md @@ -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. diff --git a/nixos/modules/system/boot/clevis.nix b/nixos/modules/system/boot/clevis.nix new file mode 100644 index 000000000000..0c72590f9385 --- /dev/null +++ b/nixos/modules/system/boot/clevis.nix @@ -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" + ]; + }; + }; + }; +} diff --git a/nixos/modules/system/boot/luksroot.nix b/nixos/modules/system/boot/luksroot.nix index ca560d63f3bd..8bd9e71cb3a9 100644 --- a/nixos/modules/system/boot/luksroot.nix +++ b/nixos/modules/system/boot/luksroot.nix @@ -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 ]; }; } diff --git a/nixos/modules/tasks/filesystems/bcachefs.nix b/nixos/modules/tasks/filesystems/bcachefs.nix index f28fd5cde9c1..639ff87841b6 100644 --- a/nixos/modules/tasks/filesystems/bcachefs.nix +++ b/nixos/modules/tasks/filesystems/bcachefs.nix @@ -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} ''; diff --git a/nixos/modules/tasks/filesystems/zfs.nix b/nixos/modules/tasks/filesystems/zfs.nix index 72bc79f31b68..fd92a0014002 100644 --- a/nixos/modules/tasks/filesystems/zfs.nix +++ b/nixos/modules/tasks/filesystems/zfs.nix @@ -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 diff --git a/pkgs/tools/security/clevis/default.nix b/pkgs/tools/security/clevis/default.nix index 0498f0599052..fa1be45a5b99 100644 --- a/pkgs/tools/security/clevis/default.nix +++ b/pkgs/tools/security/clevis/default.nix @@ -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"; diff --git a/pkgs/tools/security/clevis/tang-timeout.patch b/pkgs/tools/security/clevis/tang-timeout.patch new file mode 100644 index 000000000000..454541d5e77c --- /dev/null +++ b/pkgs/tools/security/clevis/tang-timeout.patch @@ -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