mirror of
https://github.com/NixOS/nixpkgs.git
synced 2024-11-25 16:33:15 +00:00
nixos/systemd-sysusers: stop creating users statically
On Linux we cannot feasbibly generate users statically because we need to take care to not change or re-use UIDs over the lifetime of a machine (i.e. over multiple generations). This means we need the context of the running machine. Thus, stop creating users statically and instead generate them at runtime irrespective of mutableUsers. When /etc is immutable, the password files (e.g. /etc/passwd etc.) are created in a separate directory (/var/lib/nixos/etc). /etc will be pre-populated with symlinks to this separate directory. Immutable users are now implemented by bind-mounting the password files read-only onto themselves and only briefly re-mounting them writable to re-execute sysusers. The biggest limitation of this design is that you now need to manually unmount this bind mount to change passwords because sysusers cannot change passwords for you. This shouldn't be too much of an issue because system users should only rarely need to change their passwords.
This commit is contained in:
parent
d43e323b4a
commit
2710a49adb
@ -32,32 +32,12 @@ let
|
|||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
staticSysusersCredentials = pkgs.runCommand "static-sysusers-credentials" { } ''
|
immutableEtc = config.system.etc.overlay.enable && !config.system.etc.overlay.mutable;
|
||||||
mkdir $out; cd $out
|
# The location of the password files when using an immutable /etc.
|
||||||
${lib.concatLines (
|
immutablePasswordFilesLocation = "/var/lib/nixos/etc";
|
||||||
(lib.mapAttrsToList
|
passwordFilesLocation = if immutableEtc then immutablePasswordFilesLocation else "/etc";
|
||||||
(username: opts: "echo -n '${opts.initialHashedPassword}' > 'passwd.hashed-password.${username}'")
|
# The filenames created by systemd-sysusers.
|
||||||
(lib.filterAttrs (_username: opts: opts.initialHashedPassword != null) systemUsers))
|
passwordFiles = [ "passwd" "group" "shadow" "gshadow" ];
|
||||||
++
|
|
||||||
(lib.mapAttrsToList
|
|
||||||
(username: opts: "echo -n '${opts.initialPassword}' > 'passwd.plaintext-password.${username}'")
|
|
||||||
(lib.filterAttrs (_username: opts: opts.initialPassword != null) systemUsers))
|
|
||||||
++
|
|
||||||
(lib.mapAttrsToList
|
|
||||||
(username: opts: "cat '${opts.hashedPasswordFile}' > 'passwd.hashed-password.${username}'")
|
|
||||||
(lib.filterAttrs (_username: opts: opts.hashedPasswordFile != null) systemUsers))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
|
|
||||||
staticSysusers = pkgs.runCommand "static-sysusers"
|
|
||||||
{
|
|
||||||
nativeBuildInputs = [ pkgs.systemd ];
|
|
||||||
} ''
|
|
||||||
mkdir $out
|
|
||||||
export CREDENTIALS_DIRECTORY=${staticSysusersCredentials}
|
|
||||||
systemd-sysusers --root $out ${sysusersConfig}/00-nixos.conf
|
|
||||||
'';
|
|
||||||
|
|
||||||
in
|
in
|
||||||
|
|
||||||
@ -99,93 +79,100 @@ in
|
|||||||
})
|
})
|
||||||
userCfg.users;
|
userCfg.users;
|
||||||
|
|
||||||
systemd = lib.mkMerge [
|
systemd = {
|
||||||
({
|
|
||||||
|
|
||||||
# Create home directories, do not create /var/empty even if that's a user's
|
# Create home directories, do not create /var/empty even if that's a user's
|
||||||
# home.
|
# home.
|
||||||
tmpfiles.settings.home-directories = lib.mapAttrs'
|
tmpfiles.settings.home-directories = lib.mapAttrs'
|
||||||
(username: opts: lib.nameValuePair opts.home {
|
(username: opts: lib.nameValuePair opts.home {
|
||||||
d = {
|
d = {
|
||||||
mode = opts.homeMode;
|
mode = opts.homeMode;
|
||||||
user = username;
|
user = username;
|
||||||
group = opts.group;
|
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);
|
|
||||||
|
|
||||||
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.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) 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))
|
|
||||||
;
|
|
||||||
};
|
};
|
||||||
|
})
|
||||||
|
(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);
|
||||||
|
|
||||||
|
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 [
|
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;
|
"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 ];
|
meta.maintainers = with lib.maintainers; [ nikstur ];
|
||||||
|
@ -32,6 +32,18 @@
|
|||||||
with subtest("direct symlinks point to the target without indirection"):
|
with subtest("direct symlinks point to the target without indirection"):
|
||||||
assert machine.succeed("readlink -n /etc/localtime") == "/etc/zoneinfo/Utc"
|
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"):
|
with subtest("switching to the same generation"):
|
||||||
machine.succeed("/run/current-system/bin/switch-to-configuration test")
|
machine.succeed("/run/current-system/bin/switch-to-configuration test")
|
||||||
|
|
||||||
|
@ -16,9 +16,12 @@ in
|
|||||||
systemd.sysusers.enable = true;
|
systemd.sysusers.enable = true;
|
||||||
users.mutableUsers = false;
|
users.mutableUsers = false;
|
||||||
|
|
||||||
# Override the empty root password set by the test instrumentation
|
|
||||||
users.users.root.hashedPasswordFile = lib.mkForce null;
|
# Read this password file at runtime from outside the Nix store.
|
||||||
users.users.root.initialHashedPassword = rootPassword;
|
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 = {
|
users.users.sysuser = {
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
group = "wheel";
|
group = "wheel";
|
||||||
@ -37,16 +40,6 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
testScript = ''
|
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"):
|
with subtest("root user has correct password"):
|
||||||
print(machine.succeed("getent passwd root"))
|
print(machine.succeed("getent passwd root"))
|
||||||
assert "${rootPassword}" in machine.succeed("getent shadow root"), "root user password is not correct"
|
assert "${rootPassword}" in machine.succeed("getent shadow root"), "root user password is not correct"
|
||||||
@ -56,13 +49,15 @@ in
|
|||||||
assert machine.succeed("stat -c '%U' /sysuser") == "sysuser\n"
|
assert machine.succeed("stat -c '%U' /sysuser") == "sysuser\n"
|
||||||
assert "${sysuserPassword}" in machine.succeed("getent shadow sysuser"), "sysuser user password is not correct"
|
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")
|
machine.succeed("/run/current-system/specialisation/new-generation/bin/switch-to-configuration switch")
|
||||||
|
|
||||||
|
|
||||||
with subtest("new-sysuser user is created after switching to new generation"):
|
with subtest("new-sysuser user is created after switching to new generation"):
|
||||||
print(machine.succeed("getent passwd new-sysuser"))
|
print(machine.succeed("getent passwd new-sysuser"))
|
||||||
print(machine.succeed("getent shadow new-sysuser"))
|
|
||||||
assert machine.succeed("stat -c '%U' /new-sysuser") == "new-sysuser\n"
|
assert machine.succeed("stat -c '%U' /new-sysuser") == "new-sysuser\n"
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
@ -63,6 +63,9 @@ in
|
|||||||
print(machine.succeed("getent passwd sysuser"))
|
print(machine.succeed("getent passwd sysuser"))
|
||||||
assert machine.succeed("stat -c '%U' /sysuser") == "sysuser\n"
|
assert machine.succeed("stat -c '%U' /sysuser") == "sysuser\n"
|
||||||
|
|
||||||
|
with subtest("Manually add new user"):
|
||||||
|
machine.succeed("useradd manual-sysuser")
|
||||||
|
|
||||||
|
|
||||||
machine.succeed("/run/current-system/specialisation/new-generation/bin/switch-to-configuration switch")
|
machine.succeed("/run/current-system/specialisation/new-generation/bin/switch-to-configuration switch")
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user