1{
2 config,
3 lib,
4 pkgs,
5 utils,
6 ...
7}:
8
9let
10
11 cfg = config.systemd.sysusers;
12 userCfg = config.users;
13
14 systemUsers = lib.filterAttrs (_username: opts: opts.enable && !opts.isNormalUser) userCfg.users;
15
16 sysusersConfig = pkgs.writeTextDir "00-nixos.conf" ''
17 # Type Name ID GECOS Home directory Shell
18
19 # Users
20 ${lib.concatLines (
21 lib.mapAttrsToList (
22 username: opts:
23 let
24 uid = if opts.uid == null then "/var/lib/nixos/uid/${username}" else toString opts.uid;
25 in
26 ''u ${username} ${uid}:${opts.group} "${opts.description}" ${opts.home} ${utils.toShellPath opts.shell}''
27 ) systemUsers
28 )}
29
30 # Groups
31 ${lib.concatLines (
32 lib.mapAttrsToList (
33 groupname: opts:
34 ''g ${groupname} ${
35 if opts.gid == null then "/var/lib/nixos/gid/${groupname}" else toString opts.gid
36 }''
37 ) userCfg.groups
38 )}
39
40 # Group membership
41 ${lib.concatStrings (
42 lib.mapAttrsToList (
43 groupname: opts: (lib.concatMapStrings (username: "m ${username} ${groupname}\n")) opts.members
44 ) userCfg.groups
45 )}
46 '';
47
48 immutableEtc = config.system.etc.overlay.enable && !config.system.etc.overlay.mutable;
49 # The location of the password files when using an immutable /etc.
50 immutablePasswordFilesLocation = "/var/lib/nixos/etc";
51 passwordFilesLocation = if immutableEtc then immutablePasswordFilesLocation else "/etc";
52 # The filenames created by systemd-sysusers.
53 passwordFiles = [
54 "passwd"
55 "group"
56 "shadow"
57 "gshadow"
58 ];
59
60in
61
62{
63
64 options = {
65
66 # This module doesn't set it's own user options but reuses the ones from
67 # users-groups.nix
68
69 systemd.sysusers = {
70 enable = lib.mkEnableOption "systemd-sysusers" // {
71 description = ''
72 If enabled, users are created with systemd-sysusers instead of with
73 the custom `update-users-groups.pl` script.
74
75 Note: This is experimental.
76 '';
77 };
78 };
79
80 };
81
82 config = lib.mkIf cfg.enable {
83
84 assertions = [
85 {
86 assertion = config.system.activationScripts.users == "";
87 message = "system.activationScripts.users has to be empty to use systemd-sysusers";
88 }
89 ]
90 ++ (lib.mapAttrsToList (username: opts: {
91 assertion = opts.enable -> !opts.isNormalUser;
92 message = "${username} is a normal user. systemd-sysusers doesn't create normal users, only system users.";
93 }) userCfg.users)
94 ++ lib.mapAttrsToList (username: opts: {
95 assertion =
96 (opts.password == opts.initialPassword || opts.password == null)
97 && (opts.hashedPassword == opts.initialHashedPassword || opts.hashedPassword == null);
98 message = "user '${username}' uses password or hashedPassword. systemd-sysupdate only supports initial passwords. It'll never update your passwords.";
99 }) systemUsers;
100
101 systemd = {
102
103 # Create home directories, do not create /var/empty even if that's a user's
104 # home.
105 tmpfiles.settings.home-directories = lib.mapAttrs' (
106 username: opts:
107 lib.nameValuePair opts.home {
108 d = {
109 mode = opts.homeMode;
110 user = username;
111 group = opts.group;
112 };
113 }
114 ) (lib.filterAttrs (_username: opts: opts.home != "/var/empty") systemUsers);
115
116 # Create uid/gid marker files for those without an explicit id
117 tmpfiles.settings.nixos-uid = lib.mapAttrs' (
118 username: opts:
119 lib.nameValuePair "/var/lib/nixos/uid/${username}" {
120 f = {
121 user = username;
122 };
123 }
124 ) (lib.filterAttrs (_username: opts: opts.uid == null) systemUsers);
125
126 tmpfiles.settings.nixos-gid = lib.mapAttrs' (
127 groupname: opts:
128 lib.nameValuePair "/var/lib/nixos/gid/${groupname}" {
129 f = {
130 group = groupname;
131 };
132 }
133 ) (lib.filterAttrs (_groupname: opts: opts.gid == null) userCfg.groups);
134
135 additionalUpstreamSystemUnits = [
136 "systemd-sysusers.service"
137 ];
138
139 services.systemd-sysusers = {
140 # Enable switch-to-configuration to restart the service.
141 unitConfig.ConditionNeedsUpdate = [ "" ];
142 requiredBy = [ "sysinit-reactivation.target" ];
143 before = [ "sysinit-reactivation.target" ];
144 restartTriggers = [ "${config.environment.etc."sysusers.d".source}" ];
145
146 serviceConfig = {
147 # When we have an immutable /etc we cannot write the files directly
148 # to /etc so we write it to a different directory and symlink them
149 # into /etc.
150 #
151 # We need to explicitly list the config file, otherwise
152 # systemd-sysusers cannot find it when we also pass another flag.
153 ExecStart = lib.mkIf immutableEtc [
154 ""
155 "${config.systemd.package}/bin/systemd-sysusers --root ${builtins.dirOf immutablePasswordFilesLocation} /etc/sysusers.d/00-nixos.conf"
156 ];
157
158 # Make the source files writable before executing sysusers.
159 ExecStartPre = lib.mkIf (!userCfg.mutableUsers) (
160 lib.map (file: "-${pkgs.util-linux}/bin/umount ${passwordFilesLocation}/${file}") passwordFiles
161 );
162 # Make the source files read-only after sysusers has finished.
163 ExecStartPost = lib.mkIf (!userCfg.mutableUsers) (
164 lib.map (
165 file:
166 "${pkgs.util-linux}/bin/mount --bind -o ro ${passwordFilesLocation}/${file} ${passwordFilesLocation}/${file}"
167 ) passwordFiles
168 );
169
170 LoadCredential = lib.mapAttrsToList (
171 username: opts: "passwd.hashed-password.${username}:${opts.hashedPasswordFile}"
172 ) (lib.filterAttrs (_username: opts: opts.hashedPasswordFile != null) systemUsers);
173 SetCredential =
174 (lib.mapAttrsToList (
175 username: opts: "passwd.hashed-password.${username}:${opts.initialHashedPassword}"
176 ) (lib.filterAttrs (_username: opts: opts.initialHashedPassword != null) systemUsers))
177 ++ (lib.mapAttrsToList (
178 username: opts: "passwd.plaintext-password.${username}:${opts.initialPassword}"
179 ) (lib.filterAttrs (_username: opts: opts.initialPassword != null) systemUsers));
180 };
181 };
182
183 };
184
185 environment.etc = lib.mkMerge [
186 ({
187 "sysusers.d".source = sysusersConfig;
188 })
189
190 # Statically create the symlinks to immutablePasswordFilesLocation when
191 # using an immutable /etc because we will not be able to do it at
192 # runtime!
193 (lib.mkIf immutableEtc (
194 lib.listToAttrs (
195 lib.map (
196 file:
197 lib.nameValuePair file {
198 source = "${immutablePasswordFilesLocation}/${file}";
199 mode = "direct-symlink";
200 }
201 ) passwordFiles
202 )
203 ))
204 ];
205 };
206
207 meta.maintainers = with lib.maintainers; [ nikstur ];
208
209}