Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sysusers only for sysusers #328926

Merged
merged 3 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 108 additions & 107 deletions nixos/modules/system/boot/systemd/sysusers.nix
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ let
cfg = config.systemd.sysusers;
userCfg = config.users;

systemUsers = lib.filterAttrs (_username: opts: !opts.isNormalUser) userCfg.users;
nikstur marked this conversation as resolved.
Show resolved Hide resolved

sysusersConfig = pkgs.writeTextDir "00-nixos.conf" ''
# Type Name ID GECOS Home directory Shell

Expand All @@ -16,7 +18,7 @@ let
in
''u ${username} ${uid}:${opts.group} "${opts.description}" ${opts.home} ${utils.toShellPath opts.shell}''
)
userCfg.users)
systemUsers)
}

# Groups
Expand All @@ -30,32 +32,12 @@ let
}
'';

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
'';
immutableEtc = config.system.etc.overlay.enable && !config.system.etc.overlay.mutable;
# The location of the password files when using an immutable /etc.
immutablePasswordFilesLocation = "/var/lib/nixos/etc";
passwordFilesLocation = if immutableEtc then immutablePasswordFilesLocation else "/etc";
# The filenames created by systemd-sysusers.
passwordFiles = [ "passwd" "group" "shadow" "gshadow" ];

in

Expand Down Expand Up @@ -90,95 +72,114 @@ in
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);

# Create uid/gid marker files for those without an explicit id
tmpfiles.settings.nixos-uid = lib.mapAttrs'
(username: opts: lib.nameValuePair "/var/lib/nixos/uid/${username}" {
f = {
user = username;
};
})
(lib.filterAttrs (_username: opts: opts.uid == null) userCfg.users);

tmpfiles.settings.nixos-gid = lib.mapAttrs'
(groupname: opts: lib.nameValuePair "/var/lib/nixos/gid/${groupname}" {
f = {
group = groupname;
};
})
(lib.filterAttrs (_groupname: opts: opts.gid == null) userCfg.groups);
] ++ (lib.mapAttrsToList
(_username: opts: {
assertion = !opts.isNormalUser;
message = "systemd-sysusers doesn't create normal users. You can currently only use it to create system users.";
})
userCfg.users)
++ lib.mapAttrsToList
(username: opts: {
assertion = (opts.password == opts.initialPassword || opts.password == null) &&
(opts.hashedPassword == opts.initialHashedPassword || opts.hashedPassword == null);
message = "${username} uses password or hashedPassword. systemd-sysupdate only supports initial passwords. It'll never update your passwords.";
})
systemUsers;

systemd = {

# 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") systemUsers);

# Create uid/gid marker files for those without an explicit id
tmpfiles.settings.nixos-uid = lib.mapAttrs'
(username: opts: lib.nameValuePair "/var/lib/nixos/uid/${username}" {
f = {
user = username;
};
})
(lib.filterAttrs (_username: opts: opts.uid == null) systemUsers);

(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))
;
tmpfiles.settings.nixos-gid = lib.mapAttrs'
(groupname: opts: lib.nameValuePair "/var/lib/nixos/gid/${groupname}" {
f = {
group = groupname;
};
})
(lib.filterAttrs (_groupname: opts: opts.gid == null) userCfg.groups);

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 = {
# When we have an immutable /etc we cannot write the files directly
# to /etc so we write it to a different directory and symlink them
# into /etc.
#
# We need to explicitly list the config file, otherwise
# systemd-sysusers cannot find it when we also pass another flag.
ExecStart = lib.mkIf immutableEtc
[ "" "${config.systemd.package}/bin/systemd-sysusers --root ${builtins.dirOf immutablePasswordFilesLocation} /etc/sysusers.d/00-nixos.conf" ];

# Make the source files writable before executing sysusers.
ExecStartPre = lib.mkIf (!userCfg.mutableUsers)
(lib.map
(file: "-${pkgs.util-linux}/bin/umount ${passwordFilesLocation}/${file}")
passwordFiles);
# Make the source files read-only after sysusers has finished.
ExecStartPost = lib.mkIf (!userCfg.mutableUsers)
(lib.map
(file: "${pkgs.util-linux}/bin/mount --bind -o ro ${passwordFilesLocation}/${file} ${passwordFilesLocation}/${file}")
passwordFiles);

LoadCredential = lib.mapAttrsToList
(username: opts: "passwd.hashed-password.${username}:${opts.hashedPasswordFile}")
(lib.filterAttrs (_username: opts: opts.hashedPasswordFile != null) systemUsers);
SetCredential = (lib.mapAttrsToList
(username: opts: "passwd.hashed-password.${username}:${opts.initialHashedPassword}")
(lib.filterAttrs (_username: opts: opts.initialHashedPassword != null) systemUsers))
++
(lib.mapAttrsToList
(username: opts: "passwd.plaintext-password.${username}:${opts.initialPassword}")
(lib.filterAttrs (_username: opts: opts.initialPassword != null) systemUsers))
;
};
})
];
};

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 {
environment.etc = lib.mkMerge [
({
"sysusers.d".source = sysusersConfig;
})
];

# Statically create the symlinks to immutablePasswordFilesLocation when
# using an immutable /etc because we will not be able to do it at
# runtime!
(lib.mkIf immutableEtc (lib.listToAttrs (lib.map
(file: lib.nameValuePair file {
source = "${immutablePasswordFilesLocation}/${file}";
mode = "direct-symlink";
})
passwordFiles)))
];
};

meta.maintainers = with lib.maintainers; [ nikstur ];
Expand Down
12 changes: 12 additions & 0 deletions nixos/tests/activation/etc-overlay-immutable.nix
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@
with subtest("direct symlinks point to the target without indirection"):
assert machine.succeed("readlink -n /etc/localtime") == "/etc/zoneinfo/Utc"

with subtest("Correct mode on the source password files"):
assert machine.succeed("stat -c '%a' /var/lib/nixos/etc/passwd") == "644\n"
assert machine.succeed("stat -c '%a' /var/lib/nixos/etc/group") == "644\n"
assert machine.succeed("stat -c '%a' /var/lib/nixos/etc/shadow") == "0\n"
assert machine.succeed("stat -c '%a' /var/lib/nixos/etc/gshadow") == "0\n"

with subtest("Password files are symlinks to /var/lib/nixos/etc"):
assert machine.succeed("readlink -f /etc/passwd") == "/var/lib/nixos/etc/passwd\n"
assert machine.succeed("readlink -f /etc/group") == "/var/lib/nixos/etc/group\n"
assert machine.succeed("readlink -f /etc/shadow") == "/var/lib/nixos/etc/shadow\n"
assert machine.succeed("readlink -f /etc/gshadow") == "/var/lib/nixos/etc/gshadow\n"

with subtest("switching to the same generation"):
machine.succeed("/run/current-system/bin/switch-to-configuration test")

Expand Down
57 changes: 28 additions & 29 deletions nixos/tests/systemd-sysusers-immutable.nix
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

let
rootPassword = "$y$j9T$p6OI0WN7.rSfZBOijjRdR.$xUOA2MTcB48ac.9Oc5fz8cxwLv1mMqabnn333iOzSA6";
normaloPassword = "$y$j9T$3aiOV/8CADAK22OK2QT3/0$67OKd50Z4qTaZ8c/eRWHLIM.o3ujtC1.n9ysmJfv639";
newNormaloPassword = "mellow";
sysuserPassword = "$y$j9T$3aiOV/8CADAK22OK2QT3/0$67OKd50Z4qTaZ8c/eRWHLIM.o3ujtC1.n9ysmJfv639";
newSysuserPassword = "mellow";
in

{
Expand All @@ -16,49 +16,48 @@ in
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;

# Read this password file at runtime from outside the Nix store.
environment.etc."rootpw.secret".text = rootPassword;
# Override the empty root password set by the test instrumentation.
users.users.root.hashedPasswordFile = lib.mkForce "/etc/rootpw.secret";

users.users.sysuser = {
isSystemUser = true;
group = "wheel";
home = "/sysuser";
initialHashedPassword = sysuserPassword;
};

specialisation.new-generation.configuration = {
users.users.new-normalo = {
isNormalUser = true;
initialPassword = newNormaloPassword;
users.users.new-sysuser = {
isSystemUser = true;
group = "wheel";
home = "/new-sysuser";
initialPassword = newSysuserPassword;
};
};
};

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"
with subtest("sysuser user is created"):
print(machine.succeed("getent passwd sysuser"))
assert machine.succeed("stat -c '%U' /sysuser") == "sysuser\n"
assert "${sysuserPassword}" in machine.succeed("getent shadow sysuser"), "sysuser user password is not correct"

with subtest("Fail to add new user manually"):
machine.fail("useradd manual-sysuser")


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"
with subtest("new-sysuser user is created after switching to new generation"):
print(machine.succeed("getent passwd new-sysuser"))
assert machine.succeed("stat -c '%U' /new-sysuser") == "new-sysuser\n"
'';
}
Loading