From 09f4bf7f166d4bf2e09b1923ae5744e6193caf44 Mon Sep 17 00:00:00 2001 From: Nicola Squartini Date: Sat, 29 Apr 2023 19:35:29 +0200 Subject: [PATCH 1/4] nixos/pam: enable unlocking ZFS home dataset --- .../manual/release-notes/rl-2305.section.md | 2 + nixos/modules/security/pam.nix | 55 ++++++++++++++++++- pkgs/os-specific/linux/zfs/generic.nix | 4 +- 3 files changed, 58 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 c96931233499..4a83134fec36 100644 --- a/nixos/doc/manual/release-notes/rl-2305.section.md +++ b/nixos/doc/manual/release-notes/rl-2305.section.md @@ -411,6 +411,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 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..75f8c174940c 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 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 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 contacenated with + `"/" + ` 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. @@ -1378,7 +1426,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/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" From 5466f767556b12a6db4bcaa6e9ab9970d28b0d29 Mon Sep 17 00:00:00 2001 From: Nicola Squartini Date: Sun, 30 Apr 2023 11:32:16 +0200 Subject: [PATCH 2/4] nixos/pam: improve documentation of ZFS module --- nixos/doc/manual/release-notes/rl-2305.section.md | 2 +- nixos/modules/security/pam.nix | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nixos/doc/manual/release-notes/rl-2305.section.md b/nixos/doc/manual/release-notes/rl-2305.section.md index 4a83134fec36..a836ed99eb06 100644 --- a/nixos/doc/manual/release-notes/rl-2305.section.md +++ b/nixos/doc/manual/release-notes/rl-2305.section.md @@ -411,7 +411,7 @@ In addition to numerous new and upgraded packages, this release has the followin } ``` -- New option `security.pam.zfs` to enable unlocking and mounting of ZFS home dataset at login. +- 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: diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix index 75f8c174940c..55137df7afe4 100644 --- a/nixos/modules/security/pam.nix +++ b/nixos/modules/security/pam.nix @@ -451,7 +451,7 @@ let defaultText = literalExpression "config.security.pam.zfs.enable"; type = types.bool; description = lib.mdDoc '' - Enable unlocking and mounting of ZFS home dataset at login. + Enable unlocking and mounting of encrypted ZFS home dataset at login. ''; }; @@ -1227,7 +1227,7 @@ in default = false; type = types.bool; description = lib.mdDoc '' - Enable unlocking and mounting of ZFS home dataset at login. + Enable unlocking and mounting of encrypted ZFS home dataset at login. ''; }; @@ -1236,7 +1236,7 @@ in default = "rpool/home"; type = types.str; description = lib.mdDoc '' - Prefix of home datasets. This value will be contacenated with + Prefix of home datasets. This value will be concatenated with `"/" + ` in order to determine the home dataset to unlock. ''; }; From 87cbaf7ce339136ccbd639b7b5a1cf988d0fa440 Mon Sep 17 00:00:00 2001 From: Nicola Squartini Date: Sun, 30 Apr 2023 12:07:45 +0200 Subject: [PATCH 3/4] nixos/pam: assert ZFS support for PAM module --- nixos/modules/security/pam.nix | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix index 55137df7afe4..41994aa52422 100644 --- a/nixos/modules/security/pam.nix +++ b/nixos/modules/security/pam.nix @@ -1286,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 = From 56e894b0b137b223d297896b13c84a12345b58d5 Mon Sep 17 00:00:00 2001 From: Nicola Squartini Date: Sun, 14 May 2023 21:30:01 +0200 Subject: [PATCH 4/4] nixos/pam: add test for ZFS home dataset unlocking --- nixos/tests/all-tests.nix | 1 + nixos/tests/pam/zfs-key.nix | 83 +++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 nixos/tests/pam/zfs-key.nix 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") + ''; + } +)