1{ config, pkgs, lib, ... }:
2
3with lib;
4
5let
6 cfg = config.users.mysql;
7in
8{
9 options = {
10 users.mysql = {
11 enable = mkEnableOption (lib.mdDoc "Authentication against a MySQL/MariaDB database");
12 host = mkOption {
13 type = types.str;
14 example = "localhost";
15 description = lib.mdDoc "The hostname of the MySQL/MariaDB server";
16 };
17 database = mkOption {
18 type = types.str;
19 example = "auth";
20 description = lib.mdDoc "The name of the database containing the users";
21 };
22 user = mkOption {
23 type = types.str;
24 example = "nss-user";
25 description = lib.mdDoc "The username to use when connecting to the database";
26 };
27 passwordFile = mkOption {
28 type = types.path;
29 example = "/run/secrets/mysql-auth-db-passwd";
30 description = lib.mdDoc "The path to the file containing the password for the user";
31 };
32 pam = mkOption {
33 description = lib.mdDoc "Settings for `pam_mysql`";
34 type = types.submodule {
35 options = {
36 table = mkOption {
37 type = types.str;
38 example = "users";
39 description = lib.mdDoc "The name of table that maps unique login names to the passwords.";
40 };
41 updateTable = mkOption {
42 type = types.nullOr types.str;
43 default = null;
44 example = "users_updates";
45 description = lib.mdDoc ''
46 The name of the table used for password alteration. If not defined, the value
47 of the `table` option will be used instead.
48 '';
49 };
50 userColumn = mkOption {
51 type = types.str;
52 example = "username";
53 description = lib.mdDoc "The name of the column that contains a unix login name.";
54 };
55 passwordColumn = mkOption {
56 type = types.str;
57 example = "password";
58 description = lib.mdDoc "The name of the column that contains a (encrypted) password string.";
59 };
60 statusColumn = mkOption {
61 type = types.nullOr types.str;
62 default = null;
63 example = "status";
64 description = lib.mdDoc ''
65 The name of the column or an SQL expression that indicates the status of
66 the user. The status is expressed by the combination of two bitfields
67 shown below:
68
69 - `bit 0 (0x01)`:
70 if flagged, `pam_mysql` deems the account to be expired and
71 returns `PAM_ACCT_EXPIRED`. That is, the account is supposed
72 to no longer be available. Note this doesn't mean that `pam_mysql`
73 rejects further authentication operations.
74 - `bit 1 (0x02)`:
75 if flagged, `pam_mysql` deems the authentication token
76 (password) to be expired and returns `PAM_NEW_AUTHTOK_REQD`.
77 This ends up requiring that the user enter a new password.
78 '';
79 };
80 passwordCrypt = mkOption {
81 example = "2";
82 type = types.enum [
83 "0" "plain"
84 "1" "Y"
85 "2" "mysql"
86 "3" "md5"
87 "4" "sha1"
88 "5" "drupal7"
89 "6" "joomla15"
90 "7" "ssha"
91 "8" "sha512"
92 "9" "sha256"
93 ];
94 description = lib.mdDoc ''
95 The method to encrypt the user's password:
96
97 - `0` (or `"plain"`):
98 No encryption. Passwords are stored in plaintext. HIGHLY DISCOURAGED.
99 - `1` (or `"Y"`):
100 Use crypt(3) function.
101 - `2` (or `"mysql"`):
102 Use the MySQL PASSWORD() function. It is possible that the encryption function used
103 by `pam_mysql` is different from that of the MySQL server, as
104 `pam_mysql` uses the function defined in MySQL's C-client API
105 instead of using PASSWORD() SQL function in the query.
106 - `3` (or `"md5"`):
107 Use plain hex MD5.
108 - `4` (or `"sha1"`):
109 Use plain hex SHA1.
110 - `5` (or `"drupal7"`):
111 Use Drupal7 salted passwords.
112 - `6` (or `"joomla15"`):
113 Use Joomla15 salted passwords.
114 - `7` (or `"ssha"`):
115 Use ssha hashed passwords.
116 - `8` (or `"sha512"`):
117 Use sha512 hashed passwords.
118 - `9` (or `"sha256"`):
119 Use sha256 hashed passwords.
120 '';
121 };
122 cryptDefault = mkOption {
123 type = types.nullOr (types.enum [ "md5" "sha256" "sha512" "blowfish" ]);
124 default = null;
125 example = "blowfish";
126 description = lib.mdDoc "The default encryption method to use for `passwordCrypt = 1`.";
127 };
128 where = mkOption {
129 type = types.nullOr types.str;
130 default = null;
131 example = "host.name='web' AND user.active=1";
132 description = lib.mdDoc "Additional criteria for the query.";
133 };
134 verbose = mkOption {
135 type = types.bool;
136 default = false;
137 description = lib.mdDoc ''
138 If enabled, produces logs with detailed messages that describes what
139 `pam_mysql` is doing. May be useful for debugging.
140 '';
141 };
142 disconnectEveryOperation = mkOption {
143 type = types.bool;
144 default = false;
145 description = lib.mdDoc ''
146 By default, `pam_mysql` keeps the connection to the MySQL
147 database until the session is closed. If this option is set to true it
148 disconnects every time the PAM operation has finished. This option may
149 be useful in case the session lasts quite long.
150 '';
151 };
152 logging = {
153 enable = mkOption {
154 type = types.bool;
155 default = false;
156 description = lib.mdDoc "Enables logging of authentication attempts in the MySQL database.";
157 };
158 table = mkOption {
159 type = types.str;
160 example = "logs";
161 description = lib.mdDoc "The name of the table to which logs are written.";
162 };
163 msgColumn = mkOption {
164 type = types.str;
165 example = "msg";
166 description = lib.mdDoc ''
167 The name of the column in the log table to which the description
168 of the performed operation is stored.
169 '';
170 };
171 userColumn = mkOption {
172 type = types.str;
173 example = "user";
174 description = lib.mdDoc ''
175 The name of the column in the log table to which the name of the
176 user being authenticated is stored.
177 '';
178 };
179 pidColumn = mkOption {
180 type = types.str;
181 example = "pid";
182 description = lib.mdDoc ''
183 The name of the column in the log table to which the pid of the
184 process utilising the `pam_mysql's` authentication
185 service is stored.
186 '';
187 };
188 hostColumn = mkOption {
189 type = types.str;
190 example = "host";
191 description = lib.mdDoc ''
192 The name of the column in the log table to which the name of the user
193 being authenticated is stored.
194 '';
195 };
196 rHostColumn = mkOption {
197 type = types.str;
198 example = "rhost";
199 description = lib.mdDoc ''
200 The name of the column in the log table to which the name of the remote
201 host that initiates the session is stored. The value is supposed to be
202 set by the PAM-aware application with `pam_set_item(PAM_RHOST)`.
203 '';
204 };
205 timeColumn = mkOption {
206 type = types.str;
207 example = "timestamp";
208 description = lib.mdDoc ''
209 The name of the column in the log table to which the timestamp of the
210 log entry is stored.
211 '';
212 };
213 };
214 };
215 };
216 };
217 nss = mkOption {
218 description = lib.mdDoc ''
219 Settings for `libnss-mysql`.
220
221 All examples are from the [minimal example](https://github.com/saknopper/libnss-mysql/tree/master/sample/minimal)
222 of `libnss-mysql`, but they are modified with NixOS paths for bash.
223 '';
224 type = types.submodule {
225 options = {
226 getpwnam = mkOption {
227 type = types.nullOr types.str;
228 default = null;
229 example = literalExpression ''
230 SELECT username,'x',uid,'5000','MySQL User', CONCAT('/home/',username),'/run/sw/current-system/bin/bash' \
231 FROM users \
232 WHERE username='%1$s' \
233 LIMIT 1
234 '';
235 description = lib.mdDoc ''
236 SQL query for the [getpwnam](https://man7.org/linux/man-pages/man3/getpwnam.3.html)
237 syscall.
238 '';
239 };
240 getpwuid = mkOption {
241 type = types.nullOr types.str;
242 default = null;
243 example = literalExpression ''
244 SELECT username,'x',uid,'5000','MySQL User', CONCAT('/home/',username),'/run/sw/current-system/bin/bash' \
245 FROM users \
246 WHERE uid='%1$u' \
247 LIMIT 1
248 '';
249 description = lib.mdDoc ''
250 SQL query for the [getpwuid](https://man7.org/linux/man-pages/man3/getpwuid.3.html)
251 syscall.
252 '';
253 };
254 getspnam = mkOption {
255 type = types.nullOr types.str;
256 default = null;
257 example = literalExpression ''
258 SELECT username,password,'1','0','99999','0','0','-1','0' \
259 FROM users \
260 WHERE username='%1$s' \
261 LIMIT 1
262 '';
263 description = lib.mdDoc ''
264 SQL query for the [getspnam](https://man7.org/linux/man-pages/man3/getspnam.3.html)
265 syscall.
266 '';
267 };
268 getpwent = mkOption {
269 type = types.nullOr types.str;
270 default = null;
271 example = literalExpression ''
272 SELECT username,'x',uid,'5000','MySQL User', CONCAT('/home/',username),'/run/sw/current-system/bin/bash' FROM users
273 '';
274 description = lib.mdDoc ''
275 SQL query for the [getpwent](https://man7.org/linux/man-pages/man3/getpwent.3.html)
276 syscall.
277 '';
278 };
279 getspent = mkOption {
280 type = types.nullOr types.str;
281 default = null;
282 example = literalExpression ''
283 SELECT username,password,'1','0','99999','0','0','-1','0' FROM users
284 '';
285 description = lib.mdDoc ''
286 SQL query for the [getspent](https://man7.org/linux/man-pages/man3/getspent.3.html)
287 syscall.
288 '';
289 };
290 getgrnam = mkOption {
291 type = types.nullOr types.str;
292 default = null;
293 example = literalExpression ''
294 SELECT name,password,gid FROM groups WHERE name='%1$s' LIMIT 1
295 '';
296 description = lib.mdDoc ''
297 SQL query for the [getgrnam](https://man7.org/linux/man-pages/man3/getgrnam.3.html)
298 syscall.
299 '';
300 };
301 getgrgid = mkOption {
302 type = types.nullOr types.str;
303 default = null;
304 example = literalExpression ''
305 SELECT name,password,gid FROM groups WHERE gid='%1$u' LIMIT 1
306 '';
307 description = lib.mdDoc ''
308 SQL query for the [getgrgid](https://man7.org/linux/man-pages/man3/getgrgid.3.html)
309 syscall.
310 '';
311 };
312 getgrent = mkOption {
313 type = types.nullOr types.str;
314 default = null;
315 example = literalExpression ''
316 SELECT name,password,gid FROM groups
317 '';
318 description = lib.mdDoc ''
319 SQL query for the [getgrent](https://man7.org/linux/man-pages/man3/getgrent.3.html)
320 syscall.
321 '';
322 };
323 memsbygid = mkOption {
324 type = types.nullOr types.str;
325 default = null;
326 example = literalExpression ''
327 SELECT username FROM grouplist WHERE gid='%1$u'
328 '';
329 description = lib.mdDoc ''
330 SQL query for the [memsbygid](https://man7.org/linux/man-pages/man3/memsbygid.3.html)
331 syscall.
332 '';
333 };
334 gidsbymem = mkOption {
335 type = types.nullOr types.str;
336 default = null;
337 example = literalExpression ''
338 SELECT gid FROM grouplist WHERE username='%1$s'
339 '';
340 description = lib.mdDoc ''
341 SQL query for the [gidsbymem](https://man7.org/linux/man-pages/man3/gidsbymem.3.html)
342 syscall.
343 '';
344 };
345 };
346 };
347 };
348 };
349 };
350
351 config = mkIf cfg.enable {
352 system.nssModules = [ pkgs.libnss-mysql ];
353 system.nssDatabases.shadow = [ "mysql" ];
354 system.nssDatabases.group = [ "mysql" ];
355 system.nssDatabases.passwd = [ "mysql" ];
356
357 environment.etc."security/pam_mysql.conf" = {
358 user = "root";
359 group = "root";
360 mode = "0600";
361 # password will be added from password file in activation script
362 text = ''
363 users.host=${cfg.host}
364 users.db_user=${cfg.user}
365 users.database=${cfg.database}
366 users.table=${cfg.pam.table}
367 users.user_column=${cfg.pam.userColumn}
368 users.password_column=${cfg.pam.passwordColumn}
369 users.password_crypt=${cfg.pam.passwordCrypt}
370 users.disconnect_every_operation=${if cfg.pam.disconnectEveryOperation then "1" else "0"}
371 verbose=${if cfg.pam.verbose then "1" else "0"}
372 '' + optionalString (cfg.pam.cryptDefault != null) ''
373 users.use_${cfg.pam.cryptDefault}=1
374 '' + optionalString (cfg.pam.where != null) ''
375 users.where_clause=${cfg.pam.where}
376 '' + optionalString (cfg.pam.statusColumn != null) ''
377 users.status_column=${cfg.pam.statusColumn}
378 '' + optionalString (cfg.pam.updateTable != null) ''
379 users.update_table=${cfg.pam.updateTable}
380 '' + optionalString cfg.pam.logging.enable ''
381 log.enabled=true
382 log.table=${cfg.pam.logging.table}
383 log.message_column=${cfg.pam.logging.msgColumn}
384 log.pid_column=${cfg.pam.logging.pidColumn}
385 log.user_column=${cfg.pam.logging.userColumn}
386 log.host_column=${cfg.pam.logging.hostColumn}
387 log.rhost_column=${cfg.pam.logging.rHostColumn}
388 log.time_column=${cfg.pam.logging.timeColumn}
389 '';
390 };
391
392 environment.etc."libnss-mysql.cfg" = {
393 mode = "0600";
394 user = config.services.nscd.user;
395 group = config.services.nscd.group;
396 text = optionalString (cfg.nss.getpwnam != null) ''
397 getpwnam ${cfg.nss.getpwnam}
398 '' + optionalString (cfg.nss.getpwuid != null) ''
399 getpwuid ${cfg.nss.getpwuid}
400 '' + optionalString (cfg.nss.getspnam != null) ''
401 getspnam ${cfg.nss.getspnam}
402 '' + optionalString (cfg.nss.getpwent != null) ''
403 getpwent ${cfg.nss.getpwent}
404 '' + optionalString (cfg.nss.getspent != null) ''
405 getspent ${cfg.nss.getspent}
406 '' + optionalString (cfg.nss.getgrnam != null) ''
407 getgrnam ${cfg.nss.getgrnam}
408 '' + optionalString (cfg.nss.getgrgid != null) ''
409 getgrgid ${cfg.nss.getgrgid}
410 '' + optionalString (cfg.nss.getgrent != null) ''
411 getgrent ${cfg.nss.getgrent}
412 '' + optionalString (cfg.nss.memsbygid != null) ''
413 memsbygid ${cfg.nss.memsbygid}
414 '' + optionalString (cfg.nss.gidsbymem != null) ''
415 gidsbymem ${cfg.nss.gidsbymem}
416 '' + ''
417 host ${cfg.host}
418 database ${cfg.database}
419 '';
420 };
421
422 environment.etc."libnss-mysql-root.cfg" = {
423 mode = "0600";
424 user = config.services.nscd.user;
425 group = config.services.nscd.group;
426 # password will be added from password file in activation script
427 text = ''
428 username ${cfg.user}
429 '';
430 };
431
432 # Activation script to append the password from the password file
433 # to the configuration files. It also fixes the owner of the
434 # libnss-mysql-root.cfg because it is changed to root after the
435 # password is appended.
436 system.activationScripts.mysql-auth-passwords = ''
437 if [[ -r ${cfg.passwordFile} ]]; then
438 org_umask=$(umask)
439 umask 0077
440
441 conf_nss="$(mktemp)"
442 cp /etc/libnss-mysql-root.cfg $conf_nss
443 printf 'password %s\n' "$(cat ${cfg.passwordFile})" >> $conf_nss
444 mv -fT "$conf_nss" /etc/libnss-mysql-root.cfg
445 chown ${config.services.nscd.user}:${config.services.nscd.group} /etc/libnss-mysql-root.cfg
446
447 conf_pam="$(mktemp)"
448 cp /etc/security/pam_mysql.conf $conf_pam
449 printf 'users.db_passwd=%s\n' "$(cat ${cfg.passwordFile})" >> $conf_pam
450 mv -fT "$conf_pam" /etc/security/pam_mysql.conf
451
452 umask $org_umask
453 fi
454 '';
455 };
456}