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.
This commit is contained in:
Lorenz Brun 2022-05-17 20:16:22 +02:00
parent db3f2b35d3
commit f046cc0923
3 changed files with 81 additions and 0 deletions

View file

@ -526,6 +526,7 @@ let
# We use try_first_pass the second time to avoid prompting password twice # We use try_first_pass the second time to avoid prompting password twice
(optionalString (cfg.unixAuth && (optionalString (cfg.unixAuth &&
(config.security.pam.enableEcryptfs (config.security.pam.enableEcryptfs
|| config.security.pam.enableFscrypt
|| cfg.pamMount || cfg.pamMount
|| cfg.enableKwallet || cfg.enableKwallet
|| cfg.enableGnomeKeyring || cfg.enableGnomeKeyring
@ -539,6 +540,9 @@ let
optionalString config.security.pam.enableEcryptfs '' optionalString config.security.pam.enableEcryptfs ''
auth optional ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so unwrap 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 '' optionalString cfg.pamMount ''
auth optional ${pkgs.pam_mount}/lib/security/pam_mount.so disable_interactive auth optional ${pkgs.pam_mount}/lib/security/pam_mount.so disable_interactive
'' + '' +
@ -584,6 +588,9 @@ let
optionalString config.security.pam.enableEcryptfs '' optionalString config.security.pam.enableEcryptfs ''
password optional ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so 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 '' optionalString cfg.pamMount ''
password optional ${pkgs.pam_mount}/lib/security/pam_mount.so password optional ${pkgs.pam_mount}/lib/security/pam_mount.so
'' + '' +
@ -630,6 +637,14 @@ let
optionalString config.security.pam.enableEcryptfs '' optionalString config.security.pam.enableEcryptfs ''
session optional ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so 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 '' optionalString cfg.pamMount ''
session optional ${pkgs.pam_mount}/lib/security/pam_mount.so disable_interactive 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.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 { users.motd = mkOption {
default = null; default = null;
@ -1170,6 +1193,7 @@ in
++ optionals config.security.pam.enableOTPW [ pkgs.otpw ] ++ optionals config.security.pam.enableOTPW [ pkgs.otpw ]
++ optionals config.security.pam.oath.enable [ pkgs.oath-toolkit ] ++ optionals config.security.pam.oath.enable [ pkgs.oath-toolkit ]
++ optionals config.security.pam.p11.enable [ pkgs.pam_p11 ] ++ 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 ]; ++ optionals config.security.pam.u2f.enable [ pkgs.pam_u2f ];
boot.supportedFilesystems = optionals config.security.pam.enableEcryptfs [ "ecryptfs" ]; boot.supportedFilesystems = optionals config.security.pam.enableEcryptfs [ "ecryptfs" ];
@ -1211,6 +1235,9 @@ in
it complains "Cannot create session: Already running in a it complains "Cannot create session: Already running in a
session". */ session". */
runuser-l = { rootOK = true; unixAuth = false; }; runuser-l = { rootOK = true; unixAuth = false; };
} // optionalAttrs (config.security.pam.enableFscrypt) {
# Allow fscrypt to verify login passphrase
fscrypt = {};
}; };
security.apparmor.includes."abstractions/pam" = let security.apparmor.includes."abstractions/pam" = let
@ -1275,6 +1302,9 @@ in
optionalString config.security.pam.enableEcryptfs '' optionalString config.security.pam.enableEcryptfs ''
mr ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so, 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)) '' optionalString (isEnabled (cfg: cfg.pamMount)) ''
mr ${pkgs.pam_mount}/lib/security/pam_mount.so, mr ${pkgs.pam_mount}/lib/security/pam_mount.so,
'' + '' +

View file

@ -178,6 +178,7 @@ in {
ec2-config = (handleTestOn ["x86_64-linux"] ./ec2.nix {}).boot-ec2-config or {}; ec2-config = (handleTestOn ["x86_64-linux"] ./ec2.nix {}).boot-ec2-config or {};
ec2-nixops = (handleTestOn ["x86_64-linux"] ./ec2.nix {}).boot-ec2-nixops or {}; ec2-nixops = (handleTestOn ["x86_64-linux"] ./ec2.nix {}).boot-ec2-nixops or {};
ecryptfs = handleTest ./ecryptfs.nix {}; ecryptfs = handleTest ./ecryptfs.nix {};
fscrypt = handleTest ./fscrypt.nix {};
ejabberd = handleTest ./xmpp/ejabberd.nix {}; ejabberd = handleTest ./xmpp/ejabberd.nix {};
elk = handleTestOn ["x86_64-linux"] ./elk.nix {}; elk = handleTestOn ["x86_64-linux"] ./elk.nix {};
emacs-daemon = handleTest ./emacs-daemon.nix {}; emacs-daemon = handleTest ./emacs-daemon.nix {};

50
nixos/tests/fscrypt.nix Normal file
View file

@ -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()
'';
})