From 05420f34cf7b8eb6acb1e18d918b1a9a78762473 Mon Sep 17 00:00:00 2001 From: Leorize Date: Thu, 8 Dec 2022 02:49:12 -0600 Subject: [PATCH] nixos: add systemd-homed support As a start, it's not very configurable, but works pretty well. --- nixos/modules/module-list.nix | 1 + nixos/modules/security/pam.nix | 27 +++++- nixos/modules/system/boot/systemd.nix | 2 +- nixos/modules/system/boot/systemd/homed.nix | 43 +++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/systemd-homed.nix | 99 +++++++++++++++++++++ pkgs/os-specific/linux/systemd/default.nix | 2 +- pkgs/top-level/all-packages.nix | 1 + 8 files changed, 171 insertions(+), 5 deletions(-) create mode 100644 nixos/modules/system/boot/systemd/homed.nix create mode 100644 nixos/tests/systemd-homed.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 587cb2319e97..3196bfad8336 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1276,6 +1276,7 @@ ./system/boot/systemd/tmpfiles.nix ./system/boot/systemd/user.nix ./system/boot/systemd/userdbd.nix + ./system/boot/systemd/homed.nix ./system/boot/timesyncd.nix ./system/boot/tmp.nix ./system/boot/uvesafb.nix diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix index 21e1749d8503..ccc484198279 100644 --- a/nixos/modules/security/pam.nix +++ b/nixos/modules/security/pam.nix @@ -488,6 +488,9 @@ let account [success=ok ignore=ignore default=die] ${pkgs.google-guest-oslogin}/lib/security/pam_oslogin_login.so account [success=ok default=ignore] ${pkgs.google-guest-oslogin}/lib/security/pam_oslogin_admin.so '' + + optionalString config.services.homed.enable '' + account sufficient ${config.systemd.package}/lib/security/pam_systemd_home.so + '' + # The required pam_unix.so module has to come after all the sufficient modules # because otherwise, the account lookup will fail if the user does not exist # locally, for example with MySQL- or LDAP-auth. @@ -541,8 +544,10 @@ let # after it succeeds. Certain modules need to run after pam_unix # prompts the user for password so we run it once with 'optional' at an # earlier point and it will run again with 'sufficient' further down. - # We use try_first_pass the second time to avoid prompting password twice - (optionalString (cfg.unixAuth && + # We use try_first_pass the second time to avoid prompting password twice. + # + # The same principle applies to systemd-homed + (optionalString ((cfg.unixAuth || config.services.homed.enable) && (config.security.pam.enableEcryptfs || config.security.pam.enableFscrypt || cfg.pamMount @@ -553,7 +558,10 @@ let || cfg.failDelay.enable || cfg.duoSecurity.enable)) ( - '' + optionalString config.services.homed.enable '' + auth optional ${config.systemd.package}/lib/security/pam_systemd_home.so + '' + + optionalString cfg.unixAuth '' auth optional pam_unix.so ${optionalString cfg.allowNullPassword "nullok"} ${optionalString cfg.nodelay "nodelay"} likeauth '' + optionalString config.security.pam.enableEcryptfs '' @@ -584,6 +592,9 @@ let auth required ${pkgs.duo-unix}/lib/security/pam_duo.so '' )) + + optionalString config.services.homed.enable '' + auth sufficient ${config.systemd.package}/lib/security/pam_systemd_home.so + '' + optionalString cfg.unixAuth '' auth sufficient pam_unix.so ${optionalString cfg.allowNullPassword "nullok"} ${optionalString cfg.nodelay "nodelay"} likeauth try_first_pass '' + @@ -605,6 +616,10 @@ let auth required pam_deny.so # Password management. + '' + + optionalString config.services.homed.enable '' + password sufficient ${config.systemd.package}/lib/security/pam_systemd_home.so + '' + '' password sufficient pam_unix.so nullok sha512 '' + optionalString config.security.pam.enableEcryptfs '' @@ -650,6 +665,9 @@ let ++ optional (cfg.ttyAudit.enablePattern != null) "enable=${cfg.ttyAudit.enablePattern}" ++ optional (cfg.ttyAudit.disablePattern != null) "disable=${cfg.ttyAudit.disablePattern}" )) + + optionalString config.services.homed.enable '' + session required ${config.systemd.package}/lib/security/pam_systemd_home.so + '' + optionalString cfg.makeHomeDir '' session required ${pkgs.pam}/lib/security/pam_mkhomedir.so silent skel=${config.security.pam.makeHomeDir.skelDirectory} umask=0077 '' + @@ -1345,6 +1363,9 @@ in '' + optionalString config.virtualisation.lxc.lxcfs.enable '' mr ${pkgs.lxc}/lib/security/pam_cgfs.so + '' + + optionalString config.services.homed.enable '' + mr ${config.systemd.package}/lib/security/pam_systemd_home.so ''; }; diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix index e37ed8531810..679a663362b6 100644 --- a/nixos/modules/system/boot/systemd.nix +++ b/nixos/modules/system/boot/systemd.nix @@ -450,7 +450,7 @@ in (mkAfter [ "systemd" ]) ]); group = (mkMerge [ - (mkAfter [ "systemd" ]) + (mkAfter [ "[success=merge] systemd" ]) # need merge so that NSS won't stop at file-based groups ]); }; diff --git a/nixos/modules/system/boot/systemd/homed.nix b/nixos/modules/system/boot/systemd/homed.nix new file mode 100644 index 000000000000..403d1690124d --- /dev/null +++ b/nixos/modules/system/boot/systemd/homed.nix @@ -0,0 +1,43 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.services.homed; +in +{ + options.services.homed.enable = lib.mkEnableOption (lib.mdDoc '' + Enable systemd home area/user account manager + ''); + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = config.services.nscd.enable; + message = "systemd-homed requires the use of systemd nss module. services.nscd.enable must be set to true,"; + } + ]; + + systemd.additionalUpstreamSystemUnits = [ + "systemd-homed.service" + "systemd-homed-activate.service" + ]; + + # This is mentioned in homed's [Install] section. + # + # While homed appears to work without it, it's probably better + # to follow upstream recommendations. + services.userdbd.enable = lib.mkDefault true; + + systemd.services = { + systemd-homed = { + # These packages are required to manage encrypted volumes + path = config.system.fsPackages; + aliases = [ "dbus-org.freedesktop.home1.service" ]; + wantedBy = [ "multi-user.target" ]; + }; + + systemd-homed-activate = { + wantedBy = [ "systemd-homed.service" ]; + }; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 31243f59bb98..1eab790887d5 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -637,6 +637,7 @@ in { systemd-timesyncd = handleTest ./systemd-timesyncd.nix {}; systemd-misc = handleTest ./systemd-misc.nix {}; systemd-userdbd = handleTest ./systemd-userdbd.nix {}; + systemd-homed = handleTest ./systemd-homed.nix {}; tandoor-recipes = handleTest ./tandoor-recipes.nix {}; taskserver = handleTest ./taskserver.nix {}; tayga = handleTest ./tayga.nix {}; diff --git a/nixos/tests/systemd-homed.nix b/nixos/tests/systemd-homed.nix new file mode 100644 index 000000000000..ecc92e98eddc --- /dev/null +++ b/nixos/tests/systemd-homed.nix @@ -0,0 +1,99 @@ +import ./make-test-python.nix ({ pkgs, lib, ... }: +let + password = "foobar"; + newPass = "barfoo"; +in +{ + name = "systemd-homed"; + nodes.machine = { config, pkgs, ... }: { + services.homed.enable = true; + + users.users.test-normal-user = { + extraGroups = [ "wheel" ]; + isNormalUser = true; + initialPassword = password; + }; + }; + testScript = '' + def switchTTY(number): + machine.send_key(f"alt-f{number}") + machine.wait_until_succeeds(f"[ $(fgconsole) = {number} ]") + machine.wait_for_unit(f"getty@tty{number}.service") + machine.wait_until_succeeds(f"pgrep -f 'agetty.*tty{number}'") + + machine.wait_for_unit("multi-user.target") + + # Smoke test to make sure the pam changes didn't break regular users. + machine.wait_until_succeeds("pgrep -f 'agetty.*tty1'") + with subtest("login as regular user"): + switchTTY(2) + machine.wait_until_tty_matches("2", "login: ") + machine.send_chars("test-normal-user\n") + machine.wait_until_tty_matches("2", "login: test-normal-user") + machine.wait_until_tty_matches("2", "Password: ") + machine.send_chars("${password}\n") + machine.wait_until_succeeds("pgrep -u test-normal-user bash") + machine.send_chars("whoami > /tmp/1\n") + machine.wait_for_file("/tmp/1") + assert "test-normal-user" in machine.succeed("cat /tmp/1") + + with subtest("create homed encrypted user"): + # TODO: Figure out how to pass password manually. + # + # This environment variable is used for homed internal testing + # and is not documented. + machine.succeed("NEWPASSWORD=${password} homectl create --shell=/run/current-system/sw/bin/bash --storage=luks -G wheel test-homed-user") + + with subtest("login as homed user"): + switchTTY(3) + machine.wait_until_tty_matches("3", "login: ") + machine.send_chars("test-homed-user\n") + machine.wait_until_tty_matches("3", "login: test-homed-user") + machine.wait_until_tty_matches("3", "Password: ") + machine.send_chars("${password}\n") + machine.wait_until_succeeds("pgrep -t tty3 -u test-homed-user bash") + machine.send_chars("whoami > /tmp/2\n") + machine.wait_for_file("/tmp/2") + assert "test-homed-user" in machine.succeed("cat /tmp/2") + + with subtest("change homed user password"): + switchTTY(4) + machine.wait_until_tty_matches("4", "login: ") + machine.send_chars("test-homed-user\n") + machine.wait_until_tty_matches("4", "login: test-homed-user") + machine.wait_until_tty_matches("4", "Password: ") + machine.send_chars("${password}\n") + machine.wait_until_succeeds("pgrep -t tty4 -u test-homed-user bash") + machine.send_chars("passwd\n") + # homed does it in a weird order, it asks for new passes, then it asks + # for the old one. + machine.sleep(2) + machine.send_chars("${newPass}\n") + machine.sleep(2) + machine.send_chars("${newPass}\n") + machine.sleep(4) + machine.send_chars("${password}\n") + machine.wait_until_fails("pgrep -t tty4 passwd") + + @polling_condition + def not_logged_in_tty5(): + machine.fail("pgrep -t tty5 bash") + + switchTTY(5) + with not_logged_in_tty5: # type: ignore[union-attr] + machine.wait_until_tty_matches("5", "login: ") + machine.send_chars("test-homed-user\n") + machine.wait_until_tty_matches("5", "login: test-homed-user") + machine.wait_until_tty_matches("5", "Password: ") + machine.send_chars("${password}\n") + machine.wait_until_tty_matches("5", "Password incorrect or not sufficient for authentication of user test-homed-user.") + machine.wait_until_tty_matches("5", "Sorry, try again: ") + machine.send_chars("${newPass}\n") + machine.send_chars("whoami > /tmp/4\n") + machine.wait_for_file("/tmp/4") + assert "test-homed-user" in machine.succeed("cat /tmp/4") + + with subtest("homed user should be in wheel according to NSS"): + machine.succeed("userdbctl group wheel -s io.systemd.NameServiceSwitch | grep test-homed-user") + ''; +}) diff --git a/pkgs/os-specific/linux/systemd/default.nix b/pkgs/os-specific/linux/systemd/default.nix index 7c57d9edf601..47d42c39c124 100644 --- a/pkgs/os-specific/linux/systemd/default.nix +++ b/pkgs/os-specific/linux/systemd/default.nix @@ -81,7 +81,7 @@ , withDocumentation ? true , withEfi ? stdenv.hostPlatform.isEfi , withFido2 ? true -, withHomed ? false +, withHomed ? true , withHostnamed ? true , withHwdb ? true , withImportd ? !stdenv.hostPlatform.isMusl diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index 734cb670af49..bceb86fa66d9 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -25973,6 +25973,7 @@ with pkgs; withEfi = false; withFido2 = false; withHostnamed = false; + withHomed = false; withHwdb = false; withImportd = false; withLibBPF = false;