From f046cc092332d3b5d3d58736c83abc4dac68b579 Mon Sep 17 00:00:00 2001 From: Lorenz Brun Date: Tue, 17 May 2022 20:16:22 +0200 Subject: [PATCH] nixos/pam: support fscrypt login protectors fscrypt can automatically unlock directories with the user's login password. To do this it ships a PAM module which reads the user's password and loads the respective keys into the user's kernel keyring. Significant inspiration was taken from the ecryptfs implementation. --- nixos/modules/security/pam.nix | 30 ++++++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/fscrypt.nix | 50 ++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 nixos/tests/fscrypt.nix diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix index dc145d858515..6df8df32953f 100644 --- a/nixos/modules/security/pam.nix +++ b/nixos/modules/security/pam.nix @@ -526,6 +526,7 @@ let # We use try_first_pass the second time to avoid prompting password twice (optionalString (cfg.unixAuth && (config.security.pam.enableEcryptfs + || config.security.pam.enableFscrypt || cfg.pamMount || cfg.enableKwallet || cfg.enableGnomeKeyring @@ -539,6 +540,9 @@ let optionalString config.security.pam.enableEcryptfs '' auth optional ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so unwrap '' + + optionalString config.security.pam.enableFscrypt '' + auth optional ${pkgs.fscrypt-experimental}/lib/security/pam_fscrypt.so + '' + optionalString cfg.pamMount '' auth optional ${pkgs.pam_mount}/lib/security/pam_mount.so disable_interactive '' + @@ -584,6 +588,9 @@ let optionalString config.security.pam.enableEcryptfs '' password optional ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so '' + + optionalString config.security.pam.enableFscrypt '' + password optional ${pkgs.fscrypt-experimental}/lib/security/pam_fscrypt.so + '' + optionalString cfg.pamMount '' password optional ${pkgs.pam_mount}/lib/security/pam_mount.so '' + @@ -630,6 +637,14 @@ let optionalString config.security.pam.enableEcryptfs '' session optional ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so '' + + optionalString config.security.pam.enableFscrypt '' + # Work around https://github.com/systemd/systemd/issues/8598 + # Skips the pam_fscrypt module for systemd-user sessions which do not have a password + # anyways. + # See also https://github.com/google/fscrypt/issues/95 + session [success=1 default=ignore] pam_succeed_if.so service = systemd-user + session optional ${pkgs.fscrypt-experimental}/lib/security/pam_fscrypt.so + '' + optionalString cfg.pamMount '' session optional ${pkgs.pam_mount}/lib/security/pam_mount.so disable_interactive '' + @@ -1146,6 +1161,14 @@ in }; 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. + + This also enables a service at security.pam.services.fscrypt which is used by + fscrypt to verify the user's password when setting up a new protector. If you + use something other than pam_unix to verify user passwords, please remember to + adjust this PAM service. + ''); users.motd = mkOption { default = null; @@ -1170,6 +1193,7 @@ in ++ optionals config.security.pam.enableOTPW [ pkgs.otpw ] ++ optionals config.security.pam.oath.enable [ pkgs.oath-toolkit ] ++ optionals config.security.pam.p11.enable [ pkgs.pam_p11 ] + ++ optionals config.security.pam.enableFscrypt [ pkgs.fscrypt-experimental ] ++ optionals config.security.pam.u2f.enable [ pkgs.pam_u2f ]; boot.supportedFilesystems = optionals config.security.pam.enableEcryptfs [ "ecryptfs" ]; @@ -1211,6 +1235,9 @@ in it complains "Cannot create session: Already running in a session". */ runuser-l = { rootOK = true; unixAuth = false; }; + } // optionalAttrs (config.security.pam.enableFscrypt) { + # Allow fscrypt to verify login passphrase + fscrypt = {}; }; security.apparmor.includes."abstractions/pam" = let @@ -1275,6 +1302,9 @@ in optionalString config.security.pam.enableEcryptfs '' mr ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so, '' + + optionalString config.security.pam.enableFscrypt '' + mr ${pkgs.fscrypt-experimental}/lib/security/pam_fscrypt.so, + '' + optionalString (isEnabled (cfg: cfg.pamMount)) '' mr ${pkgs.pam_mount}/lib/security/pam_mount.so, '' + diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index cb3b9a248c0e..168d1454af60 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -178,6 +178,7 @@ in { ec2-config = (handleTestOn ["x86_64-linux"] ./ec2.nix {}).boot-ec2-config or {}; ec2-nixops = (handleTestOn ["x86_64-linux"] ./ec2.nix {}).boot-ec2-nixops or {}; ecryptfs = handleTest ./ecryptfs.nix {}; + fscrypt = handleTest ./fscrypt.nix {}; ejabberd = handleTest ./xmpp/ejabberd.nix {}; elk = handleTestOn ["x86_64-linux"] ./elk.nix {}; emacs-daemon = handleTest ./emacs-daemon.nix {}; diff --git a/nixos/tests/fscrypt.nix b/nixos/tests/fscrypt.nix new file mode 100644 index 000000000000..03367979359b --- /dev/null +++ b/nixos/tests/fscrypt.nix @@ -0,0 +1,50 @@ +import ./make-test-python.nix ({ ... }: +{ + name = "fscrypt"; + + nodes.machine = { pkgs, ... }: { + imports = [ ./common/user-account.nix ]; + security.pam.enableFscrypt = true; + }; + + testScript = '' + def login_as_alice(): + machine.wait_until_tty_matches("1", "login: ") + machine.send_chars("alice\n") + machine.wait_until_tty_matches("1", "Password: ") + machine.send_chars("foobar\n") + machine.wait_until_tty_matches("1", "alice\@machine") + + + def logout(): + machine.send_chars("logout\n") + machine.wait_until_tty_matches("1", "login: ") + + + machine.wait_for_unit("default.target") + + with subtest("Enable fscrypt on filesystem"): + machine.succeed("tune2fs -O encrypt /dev/vda") + machine.succeed("fscrypt setup --quiet --force --time=1ms") + + with subtest("Set up alice with an fscrypt-enabled home directory"): + machine.succeed("(echo foobar; echo foobar) | passwd alice") + machine.succeed("chown -R alice.users ~alice") + machine.succeed("echo foobar | fscrypt encrypt --skip-unlock --source=pam_passphrase --user=alice /home/alice") + + with subtest("Create file as alice"): + login_as_alice() + machine.succeed("echo hello > /home/alice/world") + logout() + # Wait for logout to be processed + machine.sleep(1) + + with subtest("File should not be readable without being logged in as alice"): + machine.fail("cat /home/alice/world") + + with subtest("File should be readable again as alice"): + login_as_alice() + machine.succeed("cat /home/alice/world") + logout() + ''; +})