about summary refs log tree commit diff
diff options
context:
space:
mode:
authorRyan Lahfa <masterancpp@gmail.com>2023-05-15 11:42:30 +0200
committerGitHub <noreply@github.com>2023-05-15 11:42:30 +0200
commit8c4a3f67b5022fc9d9721a48593160111316eb44 (patch)
tree6e8c56581ed37296dc9542f0f322e38c2dc78fd3
parent8b72abdbe8a57191b36f9901f36d1a7eecd59ec0 (diff)
parent56e894b0b137b223d297896b13c84a12345b58d5 (diff)
downloadnixlib-8c4a3f67b5022fc9d9721a48593160111316eb44.tar
nixlib-8c4a3f67b5022fc9d9721a48593160111316eb44.tar.gz
nixlib-8c4a3f67b5022fc9d9721a48593160111316eb44.tar.bz2
nixlib-8c4a3f67b5022fc9d9721a48593160111316eb44.tar.lz
nixlib-8c4a3f67b5022fc9d9721a48593160111316eb44.tar.xz
nixlib-8c4a3f67b5022fc9d9721a48593160111316eb44.tar.zst
nixlib-8c4a3f67b5022fc9d9721a48593160111316eb44.zip
Merge pull request #228956 from tensor5/pam-zfs-key
nixos/pam: enable unlocking ZFS home dataset
-rw-r--r--nixos/doc/manual/release-notes/rl-2305.section.md2
-rw-r--r--nixos/modules/security/pam.nix61
-rw-r--r--nixos/tests/all-tests.nix1
-rw-r--r--nixos/tests/pam/zfs-key.nix83
-rw-r--r--pkgs/os-specific/linux/zfs/generic.nix4
5 files changed, 148 insertions, 3 deletions
diff --git a/nixos/doc/manual/release-notes/rl-2305.section.md b/nixos/doc/manual/release-notes/rl-2305.section.md
index c7e92beae9f9..508a1661db54 100644
--- a/nixos/doc/manual/release-notes/rl-2305.section.md
+++ b/nixos/doc/manual/release-notes/rl-2305.section.md
@@ -410,6 +410,8 @@ In addition to numerous new and upgraded packages, this release has the followin
   }
   ```
 
+- New option `security.pam.zfs` to enable unlocking and mounting of encrypted ZFS home dataset at login.
+
 - `services.peertube` now requires you to specify the secret file `secrets.secretsFile`. It can be generated by running `openssl rand -hex 32`.
   Before upgrading, read the release notes for PeerTube:
     - [Release v5.0.0](https://github.com/Chocobozzz/PeerTube/releases/tag/v5.0.0)
diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix
index d379265960bb..41994aa52422 100644
--- a/nixos/modules/security/pam.nix
+++ b/nixos/modules/security/pam.nix
@@ -446,6 +446,15 @@ let
         };
       };
 
+      zfs = mkOption {
+        default = config.security.pam.zfs.enable;
+        defaultText = literalExpression "config.security.pam.zfs.enable";
+        type = types.bool;
+        description = lib.mdDoc ''
+          Enable unlocking and mounting of encrypted ZFS home dataset at login.
+        '';
+      };
+
       text = mkOption {
         type = types.nullOr types.lines;
         description = lib.mdDoc "Contents of the PAM service file.";
@@ -556,7 +565,8 @@ let
               || cfg.googleAuthenticator.enable
               || cfg.gnupg.enable
               || cfg.failDelay.enable
-              || cfg.duoSecurity.enable))
+              || cfg.duoSecurity.enable
+              || cfg.zfs))
             (
               optionalString config.services.homed.enable ''
                 auth optional ${config.systemd.package}/lib/security/pam_systemd_home.so
@@ -570,6 +580,9 @@ let
               optionalString config.security.pam.enableFscrypt ''
                 auth optional ${pkgs.fscrypt-experimental}/lib/security/pam_fscrypt.so
               '' +
+              optionalString cfg.zfs ''
+                auth optional ${config.boot.zfs.package}/lib/security/pam_zfs_key.so homes=${config.security.pam.zfs.homes}
+              '' +
               optionalString cfg.pamMount ''
                 auth optional ${pkgs.pam_mount}/lib/security/pam_mount.so disable_interactive
               '' +
@@ -628,6 +641,9 @@ let
           optionalString config.security.pam.enableFscrypt ''
             password optional ${pkgs.fscrypt-experimental}/lib/security/pam_fscrypt.so
           '' +
+          optionalString cfg.zfs ''
+            password optional ${config.boot.zfs.package}/lib/security/pam_zfs_key.so homes=${config.security.pam.zfs.homes}
+          '' +
           optionalString cfg.pamMount ''
             password optional ${pkgs.pam_mount}/lib/security/pam_mount.so
           '' +
@@ -685,6 +701,10 @@ let
             session [success=1 default=ignore] pam_succeed_if.so service = systemd-user
             session optional ${pkgs.fscrypt-experimental}/lib/security/pam_fscrypt.so
           '' +
+          optionalString cfg.zfs ''
+            session [success=1 default=ignore] pam_succeed_if.so service = systemd-user
+            session optional ${config.boot.zfs.package}/lib/security/pam_zfs_key.so homes=${config.security.pam.zfs.homes} ${optionalString config.security.pam.zfs.noUnmount "nounmount"}
+          '' +
           optionalString cfg.pamMount ''
             session optional ${pkgs.pam_mount}/lib/security/pam_mount.so disable_interactive
           '' +
@@ -1202,6 +1222,34 @@ in
       };
     };
 
+    security.pam.zfs = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = lib.mdDoc ''
+          Enable unlocking and mounting of encrypted ZFS home dataset at login.
+        '';
+      };
+
+      homes = mkOption {
+        example = "rpool/home";
+        default = "rpool/home";
+        type = types.str;
+        description = lib.mdDoc ''
+          Prefix of home datasets. This value will be concatenated with
+          `"/" + <username>` in order to determine the home dataset to unlock.
+        '';
+      };
+
+      noUnmount = mkOption {
+        default = false;
+        type = types.bool;
+        description = lib.mdDoc ''
+          Do not unmount home dataset on logout.
+        '';
+      };
+    };
+
     security.pam.enableEcryptfs = mkEnableOption (lib.mdDoc "eCryptfs PAM module (mounting ecryptfs home directory on login)");
     security.pam.enableFscrypt = mkEnableOption (lib.mdDoc ''
       Enables fscrypt to automatically unlock directories with the user's login password.
@@ -1238,6 +1286,12 @@ in
           Only one of users.motd and users.motdFile can be set.
         '';
       }
+      {
+        assertion = config.security.pam.zfs.enable && (config.boot.zfs.enabled || config.boot.zfs.enableUnstable);
+        message = ''
+          `security.pam.zfs.enable` requires enabling ZFS (`boot.zfs.enabled` or `boot.zfs.enableUnstable`).
+        '';
+      }
     ];
 
     environment.systemPackages =
@@ -1378,7 +1432,10 @@ in
         mr ${pkgs.plasma5Packages.kwallet-pam}/lib/security/pam_kwallet5.so,
       '' +
       optionalString config.virtualisation.lxc.lxcfs.enable ''
-        mr ${pkgs.lxc}/lib/security/pam_cgfs.so
+        mr ${pkgs.lxc}/lib/security/pam_cgfs.so,
+      '' +
+      optionalString (isEnabled (cfg: cfg.zfs)) ''
+        mr ${config.boot.zfs.package}/lib/security/pam_zfs_key.so,
       '' +
       optionalString config.services.homed.enable ''
         mr ${config.systemd.package}/lib/security/pam_systemd_home.so
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 2d45fbddbf01..008dfbe39377 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -564,6 +564,7 @@ in {
   pam-oath-login = handleTest ./pam/pam-oath-login.nix {};
   pam-u2f = handleTest ./pam/pam-u2f.nix {};
   pam-ussh = handleTest ./pam/pam-ussh.nix {};
+  pam-zfs-key = handleTest ./pam/zfs-key.nix {};
   pass-secret-service = handleTest ./pass-secret-service.nix {};
   patroni = handleTestOn ["x86_64-linux"] ./patroni.nix {};
   pantalaimon = handleTest ./matrix/pantalaimon.nix {};
diff --git a/nixos/tests/pam/zfs-key.nix b/nixos/tests/pam/zfs-key.nix
new file mode 100644
index 000000000000..4f54c287e91a
--- /dev/null
+++ b/nixos/tests/pam/zfs-key.nix
@@ -0,0 +1,83 @@
+import ../make-test-python.nix ({ ... }:
+
+  let
+    userPassword = "password";
+    mismatchPass = "mismatch";
+  in
+  {
+    name = "pam-zfs-key";
+
+    nodes.machine =
+      { ... }: {
+        boot.supportedFilesystems = [ "zfs" ];
+
+        networking.hostId = "12345678";
+
+        security.pam.zfs.enable = true;
+
+        users.users = {
+          alice = {
+            isNormalUser = true;
+            password = userPassword;
+          };
+          bob = {
+            isNormalUser = true;
+            password = userPassword;
+          };
+        };
+      };
+
+    testScript = { nodes, ... }:
+      let
+        homes = nodes.machine.security.pam.zfs.homes;
+        pool = builtins.head (builtins.split "/" homes);
+      in
+      ''
+        machine.wait_for_unit("multi-user.target")
+        machine.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
+
+        with subtest("Create encrypted ZFS datasets"):
+          machine.succeed("truncate -s 64M /testpool.img")
+          machine.succeed("zpool create -O canmount=off '${pool}' /testpool.img")
+          machine.succeed("zfs create -o canmount=off -p '${homes}'")
+          machine.succeed("echo ${userPassword} | zfs create -o canmount=noauto -o encryption=on -o keyformat=passphrase '${homes}/alice'")
+          machine.succeed("zfs unload-key '${homes}/alice'")
+          machine.succeed("echo ${mismatchPass} | zfs create -o canmount=noauto -o encryption=on -o keyformat=passphrase '${homes}/bob'")
+          machine.succeed("zfs unload-key '${homes}/bob'")
+
+        with subtest("Switch to tty2"):
+          machine.fail("pgrep -f 'agetty.*tty2'")
+          machine.send_key("alt-f2")
+          machine.wait_until_succeeds("[ $(fgconsole) = 2 ]")
+          machine.wait_for_unit("getty@tty2.service")
+          machine.wait_until_succeeds("pgrep -f 'agetty.*tty2'")
+
+        with subtest("Log in as user with home locked by login password"):
+          machine.wait_until_tty_matches("2", "login: ")
+          machine.send_chars("alice\n")
+          machine.wait_until_tty_matches("2", "login: alice")
+          machine.wait_until_succeeds("pgrep login")
+          machine.wait_until_tty_matches("2", "Password: ")
+          machine.send_chars("${userPassword}\n")
+          machine.wait_until_succeeds("pgrep -u alice bash")
+          machine.succeed("mount | grep ${homes}/alice")
+
+        with subtest("Switch to tty3"):
+          machine.fail("pgrep -f 'agetty.*tty3'")
+          machine.send_key("alt-f3")
+          machine.wait_until_succeeds("[ $(fgconsole) = 3 ]")
+          machine.wait_for_unit("getty@tty3.service")
+          machine.wait_until_succeeds("pgrep -f 'agetty.*tty3'")
+
+        with subtest("Log in as user with home locked by password different from login"):
+          machine.wait_until_tty_matches("3", "login: ")
+          machine.send_chars("bob\n")
+          machine.wait_until_tty_matches("3", "login: bob")
+          machine.wait_until_succeeds("pgrep login")
+          machine.wait_until_tty_matches("3", "Password: ")
+          machine.send_chars("${userPassword}\n")
+          machine.wait_until_succeeds("pgrep -u bob bash")
+          machine.fail("mount | grep ${homes}/bob")
+      '';
+  }
+)
diff --git a/pkgs/os-specific/linux/zfs/generic.nix b/pkgs/os-specific/linux/zfs/generic.nix
index 88838e8cabac..6c6759a5b9ca 100644
--- a/pkgs/os-specific/linux/zfs/generic.nix
+++ b/pkgs/os-specific/linux/zfs/generic.nix
@@ -11,6 +11,7 @@
 , smartmontools, enableMail ? false
 , sysstat, pkg-config
 , curl
+, pam
 
 # Kernel dependencies
 , kernel ? null
@@ -111,7 +112,7 @@ stdenv'.mkDerivation {
   nativeBuildInputs = [ autoreconfHook269 nukeReferences ]
     ++ optionals buildKernel (kernel.moduleBuildDependencies ++ [ perl ])
     ++ optional buildUser pkg-config;
-  buildInputs = optionals buildUser [ zlib libuuid attr libtirpc ]
+  buildInputs = optionals buildUser [ zlib libuuid attr libtirpc pam ]
     ++ optional buildUser openssl
     ++ optional buildUser curl
     ++ optional (buildUser && enablePython) python3;
@@ -136,6 +137,7 @@ stdenv'.mkDerivation {
     "--sysconfdir=/etc"
     "--localstatedir=/var"
     "--enable-systemd"
+    "--enable-pam"
   ] ++ optionals buildKernel ([
     "--with-linux=${kernel.dev}/lib/modules/${kernel.modDirVersion}/source"
     "--with-linux-obj=${kernel.dev}/lib/modules/${kernel.modDirVersion}/build"