nixos: add systemd-homed support

As a start, it's not very configurable, but works pretty well.

Leorize 05420f34 0cc87ab9

Changed files
+171 -5
nixos
modules
security
system
boot
tests
pkgs
os-specific
linux
systemd
top-level
+1
nixos/modules/module-list.nix
···
./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
+24 -3
nixos/modules/security/pam.nix
···
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.
···
# 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
···
|| 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 ''
···
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
'' +
···
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 ''
···
++ 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
'' +
···
'' +
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
'';
};
+1 -1
nixos/modules/system/boot/systemd.nix
···
(mkAfter [ "systemd" ])
]);
group = (mkMerge [
-
(mkAfter [ "systemd" ])
+
(mkAfter [ "[success=merge] systemd" ]) # need merge so that NSS won't stop at file-based groups
]);
};
+43
nixos/modules/system/boot/systemd/homed.nix
···
+
{ 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" ];
+
};
+
};
+
};
+
}
+1
nixos/tests/all-tests.nix
···
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 {};
+99
nixos/tests/systemd-homed.nix
···
+
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")
+
'';
+
})
+1 -1
pkgs/os-specific/linux/systemd/default.nix
···
, withDocumentation ? true
, withEfi ? stdenv.hostPlatform.isEfi
, withFido2 ? true
-
, withHomed ? false
+
, withHomed ? true
, withHostnamed ? true
, withHwdb ? true
, withImportd ? !stdenv.hostPlatform.isMusl
+1
pkgs/top-level/all-packages.nix
···
withEfi = false;
withFido2 = false;
withHostnamed = false;
+
withHomed = false;
withHwdb = false;
withImportd = false;
withLibBPF = false;