Unverified Commit 8c4a3f67 authored by Ryan Lahfa's avatar Ryan Lahfa Committed by GitHub
Browse files

Merge pull request #228956 from tensor5/pam-zfs-key

nixos/pam: enable unlocking ZFS home dataset
parents 8b72abdb 56e894b0
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -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)
+59 −2
Original line number Diff line number Diff line
@@ -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
+1 −0
Original line number Diff line number Diff line
@@ -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 {};
+83 −0
Original line number Diff line number Diff line
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")
      '';
  }
)
+3 −1
Original line number Diff line number Diff line
@@ -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"