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