lol

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.

+81
+30
nixos/modules/security/pam.nix
··· 526 526 # We use try_first_pass the second time to avoid prompting password twice 527 527 (optionalString (cfg.unixAuth && 528 528 (config.security.pam.enableEcryptfs 529 + || config.security.pam.enableFscrypt 529 530 || cfg.pamMount 530 531 || cfg.enableKwallet 531 532 || cfg.enableGnomeKeyring ··· 538 539 '' + 539 540 optionalString config.security.pam.enableEcryptfs '' 540 541 auth optional ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so unwrap 542 + '' + 543 + optionalString config.security.pam.enableFscrypt '' 544 + auth optional ${pkgs.fscrypt-experimental}/lib/security/pam_fscrypt.so 541 545 '' + 542 546 optionalString cfg.pamMount '' 543 547 auth optional ${pkgs.pam_mount}/lib/security/pam_mount.so disable_interactive ··· 584 588 optionalString config.security.pam.enableEcryptfs '' 585 589 password optional ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so 586 590 '' + 591 + optionalString config.security.pam.enableFscrypt '' 592 + password optional ${pkgs.fscrypt-experimental}/lib/security/pam_fscrypt.so 593 + '' + 587 594 optionalString cfg.pamMount '' 588 595 password optional ${pkgs.pam_mount}/lib/security/pam_mount.so 589 596 '' + ··· 629 636 '' + 630 637 optionalString config.security.pam.enableEcryptfs '' 631 638 session optional ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so 639 + '' + 640 + optionalString config.security.pam.enableFscrypt '' 641 + # Work around https://github.com/systemd/systemd/issues/8598 642 + # Skips the pam_fscrypt module for systemd-user sessions which do not have a password 643 + # anyways. 644 + # See also https://github.com/google/fscrypt/issues/95 645 + session [success=1 default=ignore] pam_succeed_if.so service = systemd-user 646 + session optional ${pkgs.fscrypt-experimental}/lib/security/pam_fscrypt.so 632 647 '' + 633 648 optionalString cfg.pamMount '' 634 649 session optional ${pkgs.pam_mount}/lib/security/pam_mount.so disable_interactive ··· 1146 1161 }; 1147 1162 1148 1163 security.pam.enableEcryptfs = mkEnableOption (lib.mdDoc "eCryptfs PAM module (mounting ecryptfs home directory on login)"); 1164 + security.pam.enableFscrypt = mkEnableOption (lib.mdDoc '' 1165 + Enables fscrypt to automatically unlock directories with the user's login password. 1166 + 1167 + This also enables a service at security.pam.services.fscrypt which is used by 1168 + fscrypt to verify the user's password when setting up a new protector. If you 1169 + use something other than pam_unix to verify user passwords, please remember to 1170 + adjust this PAM service. 1171 + ''); 1149 1172 1150 1173 users.motd = mkOption { 1151 1174 default = null; ··· 1170 1193 ++ optionals config.security.pam.enableOTPW [ pkgs.otpw ] 1171 1194 ++ optionals config.security.pam.oath.enable [ pkgs.oath-toolkit ] 1172 1195 ++ optionals config.security.pam.p11.enable [ pkgs.pam_p11 ] 1196 + ++ optionals config.security.pam.enableFscrypt [ pkgs.fscrypt-experimental ] 1173 1197 ++ optionals config.security.pam.u2f.enable [ pkgs.pam_u2f ]; 1174 1198 1175 1199 boot.supportedFilesystems = optionals config.security.pam.enableEcryptfs [ "ecryptfs" ]; ··· 1211 1235 it complains "Cannot create session: Already running in a 1212 1236 session". */ 1213 1237 runuser-l = { rootOK = true; unixAuth = false; }; 1238 + } // optionalAttrs (config.security.pam.enableFscrypt) { 1239 + # Allow fscrypt to verify login passphrase 1240 + fscrypt = {}; 1214 1241 }; 1215 1242 1216 1243 security.apparmor.includes."abstractions/pam" = let ··· 1274 1301 '' + 1275 1302 optionalString config.security.pam.enableEcryptfs '' 1276 1303 mr ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so, 1304 + '' + 1305 + optionalString config.security.pam.enableFscrypt '' 1306 + mr ${pkgs.fscrypt-experimental}/lib/security/pam_fscrypt.so, 1277 1307 '' + 1278 1308 optionalString (isEnabled (cfg: cfg.pamMount)) '' 1279 1309 mr ${pkgs.pam_mount}/lib/security/pam_mount.so,
+1
nixos/tests/all-tests.nix
··· 178 178 ec2-config = (handleTestOn ["x86_64-linux"] ./ec2.nix {}).boot-ec2-config or {}; 179 179 ec2-nixops = (handleTestOn ["x86_64-linux"] ./ec2.nix {}).boot-ec2-nixops or {}; 180 180 ecryptfs = handleTest ./ecryptfs.nix {}; 181 + fscrypt = handleTest ./fscrypt.nix {}; 181 182 ejabberd = handleTest ./xmpp/ejabberd.nix {}; 182 183 elk = handleTestOn ["x86_64-linux"] ./elk.nix {}; 183 184 emacs-daemon = handleTest ./emacs-daemon.nix {};
+50
nixos/tests/fscrypt.nix
··· 1 + import ./make-test-python.nix ({ ... }: 2 + { 3 + name = "fscrypt"; 4 + 5 + nodes.machine = { pkgs, ... }: { 6 + imports = [ ./common/user-account.nix ]; 7 + security.pam.enableFscrypt = true; 8 + }; 9 + 10 + testScript = '' 11 + def login_as_alice(): 12 + machine.wait_until_tty_matches("1", "login: ") 13 + machine.send_chars("alice\n") 14 + machine.wait_until_tty_matches("1", "Password: ") 15 + machine.send_chars("foobar\n") 16 + machine.wait_until_tty_matches("1", "alice\@machine") 17 + 18 + 19 + def logout(): 20 + machine.send_chars("logout\n") 21 + machine.wait_until_tty_matches("1", "login: ") 22 + 23 + 24 + machine.wait_for_unit("default.target") 25 + 26 + with subtest("Enable fscrypt on filesystem"): 27 + machine.succeed("tune2fs -O encrypt /dev/vda") 28 + machine.succeed("fscrypt setup --quiet --force --time=1ms") 29 + 30 + with subtest("Set up alice with an fscrypt-enabled home directory"): 31 + machine.succeed("(echo foobar; echo foobar) | passwd alice") 32 + machine.succeed("chown -R alice.users ~alice") 33 + machine.succeed("echo foobar | fscrypt encrypt --skip-unlock --source=pam_passphrase --user=alice /home/alice") 34 + 35 + with subtest("Create file as alice"): 36 + login_as_alice() 37 + machine.succeed("echo hello > /home/alice/world") 38 + logout() 39 + # Wait for logout to be processed 40 + machine.sleep(1) 41 + 42 + with subtest("File should not be readable without being logged in as alice"): 43 + machine.fail("cat /home/alice/world") 44 + 45 + with subtest("File should be readable again as alice"): 46 + login_as_alice() 47 + machine.succeed("cat /home/alice/world") 48 + logout() 49 + ''; 50 + })