about summary refs log tree commit diff
diff options
context:
space:
mode:
authorCamille Mondon <camillemondon@free.fr>2023-11-18 19:37:56 +0000
committerJulien Malka <julien@malka.sh>2023-12-02 11:55:47 +0000
commit27493b4d49b6d92f4baa049424cbb2fa48b4c948 (patch)
tree2b6f983b61f29ec542efbf8ae666a2f4f9081e74
parentbea9ec6d4ae820ee21bb1f8c37fd073678423180 (diff)
downloadnixlib-27493b4d49b6d92f4baa049424cbb2fa48b4c948.tar
nixlib-27493b4d49b6d92f4baa049424cbb2fa48b4c948.tar.gz
nixlib-27493b4d49b6d92f4baa049424cbb2fa48b4c948.tar.bz2
nixlib-27493b4d49b6d92f4baa049424cbb2fa48b4c948.tar.lz
nixlib-27493b4d49b6d92f4baa049424cbb2fa48b4c948.tar.xz
nixlib-27493b4d49b6d92f4baa049424cbb2fa48b4c948.tar.zst
nixlib-27493b4d49b6d92f4baa049424cbb2fa48b4c948.zip
nixos/clevis: init
Co-Authored-By: Julien Malka <julien@malka.sh>
-rw-r--r--nixos/doc/manual/release-notes/rl-2405.section.md2
-rw-r--r--nixos/modules/module-list.nix1
-rw-r--r--nixos/modules/system/boot/clevis.md51
-rw-r--r--nixos/modules/system/boot/clevis.nix107
-rw-r--r--nixos/modules/system/boot/luksroot.nix48
-rw-r--r--nixos/modules/tasks/filesystems/bcachefs.nix10
-rw-r--r--nixos/modules/tasks/filesystems/zfs.nix13
-rw-r--r--pkgs/tools/security/clevis/default.nix15
-rw-r--r--pkgs/tools/security/clevis/tang-timeout.patch13
9 files changed, 255 insertions, 5 deletions
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}
 
 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
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