nixos/systemd-sysusers: init

This commit is contained in:
nikstur 2023-11-19 23:16:14 +01:00
parent be2a4f37af
commit eec1845744
6 changed files with 313 additions and 4 deletions

View file

@ -685,7 +685,7 @@ in {
shadow.gid = ids.gids.shadow;
};
system.activationScripts.users = {
system.activationScripts.users = if !config.systemd.sysusers.enable then {
supportsDryActivation = true;
text = ''
install -m 0700 -d /root
@ -694,7 +694,7 @@ in {
${pkgs.perl.withPackages (p: [ p.FileSlurp p.JSON ])}/bin/perl \
-w ${./update-users-groups.pl} ${spec}
'';
};
} else ""; # keep around for backwards compatibility
system.activationScripts.update-lingering = let
lingerDir = "/var/lib/systemd/linger";
@ -711,7 +711,9 @@ in {
'';
# Warn about user accounts with deprecated password hashing schemes
system.activationScripts.hashes = {
# This does not work when the users and groups are created by
# systemd-sysusers because the users are created too late then.
system.activationScripts.hashes = if !config.systemd.sysusers.enable then {
deps = [ "users" ];
text = ''
users=()
@ -729,7 +731,7 @@ in {
printf ' - %s\n' "''${users[@]}"
fi
'';
};
} else ""; # keep around for backwards compatibility
# for backwards compatibility
system.activationScripts.groups = stringAfter [ "users" ] "";

View file

@ -1486,6 +1486,7 @@
./system/boot/systemd/repart.nix
./system/boot/systemd/shutdown.nix
./system/boot/systemd/sysupdate.nix
./system/boot/systemd/sysusers.nix
./system/boot/systemd/tmpfiles.nix
./system/boot/systemd/user.nix
./system/boot/systemd/userdbd.nix

View file

@ -0,0 +1,169 @@
{ config, lib, pkgs, utils, ... }:
let
cfg = config.systemd.sysusers;
userCfg = config.users;
sysusersConfig = pkgs.writeTextDir "00-nixos.conf" ''
# Type Name ID GECOS Home directory Shell
# Users
${lib.concatLines (lib.mapAttrsToList
(username: opts:
let
uid = if opts.uid == null then "-" else toString opts.uid;
in
''u ${username} ${uid}:${opts.group} "${opts.description}" ${opts.home} ${utils.toShellPath opts.shell}''
)
userCfg.users)
}
# Groups
${lib.concatLines (lib.mapAttrsToList
(groupname: opts: ''g ${groupname} ${if opts.gid == null then "-" else toString opts.gid}'') userCfg.groups)
}
# Group membership
${lib.concatStrings (lib.mapAttrsToList
(groupname: opts: (lib.concatMapStrings (username: "m ${username} ${groupname}\n")) opts.members ) userCfg.groups)
}
'';
staticSysusersCredentials = pkgs.runCommand "static-sysusers-credentials" { } ''
mkdir $out; cd $out
${lib.concatLines (
(lib.mapAttrsToList
(username: opts: "echo -n '${opts.initialHashedPassword}' > 'passwd.hashed-password.${username}'")
(lib.filterAttrs (_username: opts: opts.initialHashedPassword != null) userCfg.users))
++
(lib.mapAttrsToList
(username: opts: "echo -n '${opts.initialPassword}' > 'passwd.plaintext-password.${username}'")
(lib.filterAttrs (_username: opts: opts.initialPassword != null) userCfg.users))
++
(lib.mapAttrsToList
(username: opts: "cat '${opts.hashedPasswordFile}' > 'passwd.hashed-password.${username}'")
(lib.filterAttrs (_username: opts: opts.hashedPasswordFile != null) userCfg.users))
)
}
'';
staticSysusers = pkgs.runCommand "static-sysusers"
{
nativeBuildInputs = [ pkgs.systemd ];
} ''
mkdir $out
export CREDENTIALS_DIRECTORY=${staticSysusersCredentials}
systemd-sysusers --root $out ${sysusersConfig}/00-nixos.conf
'';
in
{
options = {
# This module doesn't set it's own user options but reuses the ones from
# users-groups.nix
systemd.sysusers = {
enable = lib.mkEnableOption (lib.mdDoc "systemd-sysusers") // {
description = lib.mdDoc ''
If enabled, users are created with systemd-sysusers instead of with
the custom `update-users-groups.pl` script.
Note: This is experimental.
'';
};
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = config.system.activationScripts.users == "";
message = "system.activationScripts.users has to be empty to use systemd-sysusers";
}
{
assertion = config.users.mutableUsers -> config.system.etc.overlay.enable;
message = "config.users.mutableUsers requires config.system.etc.overlay.enable.";
}
];
systemd = lib.mkMerge [
({
# Create home directories, do not create /var/empty even if that's a user's
# home.
tmpfiles.settings.home-directories = lib.mapAttrs'
(username: opts: lib.nameValuePair opts.home {
d = {
mode = opts.homeMode;
user = username;
group = opts.group;
};
})
(lib.filterAttrs (_username: opts: opts.home != "/var/empty") userCfg.users);
})
(lib.mkIf config.users.mutableUsers {
additionalUpstreamSystemUnits = [
"systemd-sysusers.service"
];
services.systemd-sysusers = {
# Enable switch-to-configuration to restart the service.
unitConfig.ConditionNeedsUpdate = [ "" ];
requiredBy = [ "sysinit-reactivation.target" ];
before = [ "sysinit-reactivation.target" ];
restartTriggers = [ "${config.environment.etc."sysusers.d".source}" ];
serviceConfig = {
LoadCredential = lib.mapAttrsToList
(username: opts: "passwd.hashed-password.${username}:${opts.hashedPasswordFile}")
(lib.filterAttrs (_username: opts: opts.hashedPasswordFile != null) userCfg.users);
SetCredential = (lib.mapAttrsToList
(username: opts: "passwd.hashed-password.${username}:${opts.initialHashedPassword}")
(lib.filterAttrs (_username: opts: opts.initialHashedPassword != null) userCfg.users))
++
(lib.mapAttrsToList
(username: opts: "passwd.plaintext-password.${username}:${opts.initialPassword}")
(lib.filterAttrs (_username: opts: opts.initialPassword != null) userCfg.users))
;
};
};
})
];
environment.etc = lib.mkMerge [
(lib.mkIf (!userCfg.mutableUsers) {
"passwd" = {
source = "${staticSysusers}/etc/passwd";
mode = "0644";
};
"group" = {
source = "${staticSysusers}/etc/group";
mode = "0644";
};
"shadow" = {
source = "${staticSysusers}/etc/shadow";
mode = "0000";
};
"gshadow" = {
source = "${staticSysusers}/etc/gshadow";
mode = "0000";
};
})
(lib.mkIf userCfg.mutableUsers {
"sysusers.d".source = sysusersConfig;
})
];
};
meta.maintainers = with lib.maintainers; [ nikstur ];
}

View file

@ -864,6 +864,8 @@ in {
systemd-repart = handleTest ./systemd-repart.nix {};
systemd-shutdown = handleTest ./systemd-shutdown.nix {};
systemd-sysupdate = runTest ./systemd-sysupdate.nix;
systemd-sysusers-mutable = runTest ./systemd-sysusers-mutable.nix;
systemd-sysusers-immutable = runTest ./systemd-sysusers-immutable.nix;
systemd-timesyncd = handleTest ./systemd-timesyncd.nix {};
systemd-timesyncd-nscd-dnssec = handleTest ./systemd-timesyncd-nscd-dnssec.nix {};
systemd-user-tmpfiles-rules = handleTest ./systemd-user-tmpfiles-rules.nix {};

View file

@ -0,0 +1,64 @@
{ lib, ... }:
let
rootPassword = "$y$j9T$p6OI0WN7.rSfZBOijjRdR.$xUOA2MTcB48ac.9Oc5fz8cxwLv1mMqabnn333iOzSA6";
normaloPassword = "$y$j9T$3aiOV/8CADAK22OK2QT3/0$67OKd50Z4qTaZ8c/eRWHLIM.o3ujtC1.n9ysmJfv639";
newNormaloPassword = "mellow";
in
{
name = "activation-sysusers-immutable";
meta.maintainers = with lib.maintainers; [ nikstur ];
nodes.machine = {
systemd.sysusers.enable = true;
users.mutableUsers = false;
# Override the empty root password set by the test instrumentation
users.users.root.hashedPasswordFile = lib.mkForce null;
users.users.root.initialHashedPassword = rootPassword;
users.users.normalo = {
isNormalUser = true;
initialHashedPassword = normaloPassword;
};
specialisation.new-generation.configuration = {
users.users.new-normalo = {
isNormalUser = true;
initialPassword = newNormaloPassword;
};
};
};
testScript = ''
with subtest("Users are not created with systemd-sysusers"):
machine.fail("systemctl status systemd-sysusers.service")
machine.fail("ls /etc/sysusers.d")
with subtest("Correct mode on the password files"):
assert machine.succeed("stat -c '%a' /etc/passwd") == "644\n"
assert machine.succeed("stat -c '%a' /etc/group") == "644\n"
assert machine.succeed("stat -c '%a' /etc/shadow") == "0\n"
assert machine.succeed("stat -c '%a' /etc/gshadow") == "0\n"
with subtest("root user has correct password"):
print(machine.succeed("getent passwd root"))
assert "${rootPassword}" in machine.succeed("getent shadow root"), "root user password is not correct"
with subtest("normalo user is created"):
print(machine.succeed("getent passwd normalo"))
assert machine.succeed("stat -c '%U' /home/normalo") == "normalo\n"
assert "${normaloPassword}" in machine.succeed("getent shadow normalo"), "normalo user password is not correct"
machine.succeed("/run/current-system/specialisation/new-generation/bin/switch-to-configuration switch")
with subtest("new-normalo user is created after switching to new generation"):
print(machine.succeed("getent passwd new-normalo"))
print(machine.succeed("getent shadow new-normalo"))
assert machine.succeed("stat -c '%U' /home/new-normalo") == "new-normalo\n"
'';
}

View file

@ -0,0 +1,71 @@
{ lib, ... }:
let
rootPassword = "$y$j9T$p6OI0WN7.rSfZBOijjRdR.$xUOA2MTcB48ac.9Oc5fz8cxwLv1mMqabnn333iOzSA6";
normaloPassword = "hello";
newNormaloPassword = "$y$j9T$p6OI0WN7.rSfZBOijjRdR.$xUOA2MTcB48ac.9Oc5fz8cxwLv1mMqabnn333iOzSA6";
in
{
name = "activation-sysusers-mutable";
meta.maintainers = with lib.maintainers; [ nikstur ];
nodes.machine = { pkgs, ... }: {
systemd.sysusers.enable = true;
users.mutableUsers = true;
# Prerequisites
system.etc.overlay.enable = true;
boot.initrd.systemd.enable = true;
boot.kernelPackages = pkgs.linuxPackages_latest;
# Override the empty root password set by the test instrumentation
users.users.root.hashedPasswordFile = lib.mkForce null;
users.users.root.initialHashedPassword = rootPassword;
users.users.normalo = {
isNormalUser = true;
initialPassword = normaloPassword;
};
specialisation.new-generation.configuration = {
users.users.new-normalo = {
isNormalUser = true;
initialHashedPassword = newNormaloPassword;
};
};
};
testScript = ''
machine.wait_for_unit("systemd-sysusers.service")
with subtest("systemd-sysusers.service contains the credentials"):
sysusers_service = machine.succeed("systemctl cat systemd-sysusers.service")
print(sysusers_service)
assert "SetCredential=passwd.plaintext-password.normalo:${normaloPassword}" in sysusers_service
with subtest("Correct mode on the password files"):
assert machine.succeed("stat -c '%a' /etc/passwd") == "644\n"
assert machine.succeed("stat -c '%a' /etc/group") == "644\n"
assert machine.succeed("stat -c '%a' /etc/shadow") == "0\n"
assert machine.succeed("stat -c '%a' /etc/gshadow") == "0\n"
with subtest("root user has correct password"):
print(machine.succeed("getent passwd root"))
assert "${rootPassword}" in machine.succeed("getent shadow root"), "root user password is not correct"
with subtest("normalo user is created"):
print(machine.succeed("getent passwd normalo"))
assert machine.succeed("stat -c '%U' /home/normalo") == "normalo\n"
machine.succeed("/run/current-system/specialisation/new-generation/bin/switch-to-configuration switch")
with subtest("new-normalo user is created after switching to new generation"):
print(machine.succeed("getent passwd new-normalo"))
assert machine.succeed("stat -c '%U' /home/new-normalo") == "new-normalo\n"
assert "${newNormaloPassword}" in machine.succeed("getent shadow new-normalo"), "new-normalo user password is not correct"
'';
}