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