diff --git a/nixos/modules/config/mysql.nix b/nixos/modules/config/mysql.nix new file mode 100644 index 00000000000..8e7ce2a307e --- /dev/null +++ b/nixos/modules/config/mysql.nix @@ -0,0 +1,519 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.users.mysql; +in +{ + options = { + users.mysql = { + enable = mkEnableOption "Authentication against a MySQL/MariaDB database"; + host = mkOption { + type = types.str; + example = "localhost"; + description = "The hostname of the MySQL/MariaDB server"; + }; + database = mkOption { + type = types.str; + example = "auth"; + description = "The name of the database containing the users"; + }; + user = mkOption { + type = types.str; + example = "nss-user"; + description = "The username to use when connecting to the database"; + }; + passwordFile = mkOption { + type = types.path; + example = "/run/secrets/mysql-auth-db-passwd"; + description = "The path to the file containing the password for the user"; + }; + pam = mkOption { + description = "Settings for pam_mysql"; + type = types.submodule { + options = { + table = mkOption { + type = types.str; + example = "users"; + description = "The name of table that maps unique login names to the passwords."; + }; + updateTable = mkOption { + type = types.nullOr types.str; + default = null; + example = "users_updates"; + description = '' + The name of the table used for password alteration. If not defined, the value + of the table option will be used instead. + ''; + }; + userColumn = mkOption { + type = types.str; + example = "username"; + description = "The name of the column that contains a unix login name."; + }; + passwordColumn = mkOption { + type = types.str; + example = "password"; + description = "The name of the column that contains a (encrypted) password string."; + }; + statusColumn = mkOption { + type = types.nullOr types.str; + default = null; + example = "status"; + description = '' + The name of the column or an SQL expression that indicates the status of + the user. The status is expressed by the combination of two bitfields + shown below: + + + + + bit 0 (0x01): + if flagged, pam_mysql deems the account to be expired and + returns PAM_ACCT_EXPIRED. That is, the account is supposed + to no longer be available. Note this doesn't mean that pam_mysql + rejects further authentication operations. + + + + + bit 1 (0x02): + if flagged, pam_mysql deems the authentication token + (password) to be expired and returns PAM_NEW_AUTHTOK_REQD. + This ends up requiring that the user enter a new password. + + + + ''; + }; + passwordCrypt = mkOption { + example = "2"; + type = types.enum [ + "0" "plain" + "1" "Y" + "2" "mysql" + "3" "md5" + "4" "sha1" + "5" "drupal7" + "6" "joomla15" + "7" "ssha" + "8" "sha512" + "9" "sha256" + ]; + description = '' + The method to encrypt the user's password: + + + + + 0 (or "plain"): + No encryption. Passwords are stored in plaintext. HIGHLY DISCOURAGED. + + + + + 1 (or "Y"): + Use crypt(3) function. + + + + + 2 (or "mysql"): + Use the MySQL PASSWORD() function. It is possible that the encryption function used + by pam_mysql is different from that of the MySQL server, as + pam_mysql uses the function defined in MySQL's C-client API + instead of using PASSWORD() SQL function in the query. + + + + + 3 (or "md5"): + Use plain hex MD5. + + + + + 4 (or "sha1"): + Use plain hex SHA1. + + + + + 5 (or "drupal7"): + Use Drupal7 salted passwords. + + + + + 6 (or "joomla15"): + Use Joomla15 salted passwords. + + + + + 7 (or "ssha"): + Use ssha hashed passwords. + + + + + 8 (or "sha512"): + Use sha512 hashed passwords. + + + + + 9 (or "sha256"): + Use sha256 hashed passwords. + + + + ''; + }; + cryptDefault = mkOption { + type = types.nullOr (types.enum [ "md5" "sha256" "sha512" "blowfish" ]); + default = null; + example = "blowfish"; + description = "The default encryption method to use for passwordCrypt = 1."; + }; + where = mkOption { + type = types.nullOr types.str; + default = null; + example = "host.name='web' AND user.active=1"; + description = "Additional criteria for the query."; + }; + verbose = mkOption { + type = types.bool; + default = false; + description = '' + If enabled, produces logs with detailed messages that describes what + pam_mysql is doing. May be useful for debugging. + ''; + }; + disconnectEveryOperation = mkOption { + type = types.bool; + default = false; + description = '' + By default, pam_mysql keeps the connection to the MySQL + database until the session is closed. If this option is set to true it + disconnects every time the PAM operation has finished. This option may + be useful in case the session lasts quite long. + ''; + }; + logging = { + enable = mkOption { + type = types.bool; + default = false; + description = "Enables logging of authentication attempts in the MySQL database."; + }; + table = mkOption { + type = types.str; + example = "logs"; + description = "The name of the table to which logs are written."; + }; + msgColumn = mkOption { + type = types.str; + example = "msg"; + description = '' + The name of the column in the log table to which the description + of the performed operation is stored. + ''; + }; + userColumn = mkOption { + type = types.str; + example = "user"; + description = '' + The name of the column in the log table to which the name of the + user being authenticated is stored. + ''; + }; + pidColumn = mkOption { + type = types.str; + example = "pid"; + description = '' + The name of the column in the log table to which the pid of the + process utilising the pam_mysql's authentication + service is stored. + ''; + }; + hostColumn = mkOption { + type = types.str; + example = "host"; + description = '' + The name of the column in the log table to which the name of the user + being authenticated is stored. + ''; + }; + rHostColumn = mkOption { + type = types.str; + example = "rhost"; + description = '' + The name of the column in the log table to which the name of the remote + host that initiates the session is stored. The value is supposed to be + set by the PAM-aware application with pam_set_item(PAM_RHOST) + . + ''; + }; + timeColumn = mkOption { + type = types.str; + example = "timestamp"; + description = '' + The name of the column in the log table to which the timestamp of the + log entry is stored. + ''; + }; + }; + }; + }; + }; + nss = mkOption { + description = '' + Settings for libnss-mysql. + + All examples are from the minimal example + of libnss-mysql, but they are modified with NixOS paths for bash. + ''; + type = types.submodule { + options = { + getpwnam = mkOption { + type = types.nullOr types.str; + default = null; + example = literalExpression '' + SELECT username,'x',uid,'5000','MySQL User', CONCAT('/home/',username),'/run/sw/current-system/bin/bash' \ + FROM users \ + WHERE username='%1$s' \ + LIMIT 1 + ''; + description = '' + SQL query for the getpwnam + syscall. + ''; + }; + getpwuid = mkOption { + type = types.nullOr types.str; + default = null; + example = literalExpression '' + SELECT username,'x',uid,'5000','MySQL User', CONCAT('/home/',username),'/run/sw/current-system/bin/bash' \ + FROM users \ + WHERE uid='%1$u' \ + LIMIT 1 + ''; + description = '' + SQL query for the getpwuid + syscall. + ''; + }; + getspnam = mkOption { + type = types.nullOr types.str; + default = null; + example = literalExpression '' + SELECT username,password,'1','0','99999','0','0','-1','0' \ + FROM users \ + WHERE username='%1$s' \ + LIMIT 1 + ''; + description = '' + SQL query for the getspnam + syscall. + ''; + }; + getpwent = mkOption { + type = types.nullOr types.str; + default = null; + example = literalExpression '' + SELECT username,'x',uid,'5000','MySQL User', CONCAT('/home/',username),'/run/sw/current-system/bin/bash' FROM users + ''; + description = '' + SQL query for the getpwent + syscall. + ''; + }; + getspent = mkOption { + type = types.nullOr types.str; + default = null; + example = literalExpression '' + SELECT username,password,'1','0','99999','0','0','-1','0' FROM users + ''; + description = '' + SQL query for the getspent + syscall. + ''; + }; + getgrnam = mkOption { + type = types.nullOr types.str; + default = null; + example = literalExpression '' + SELECT name,password,gid FROM groups WHERE name='%1$s' LIMIT 1 + ''; + description = '' + SQL query for the getgrnam + syscall. + ''; + }; + getgrgid = mkOption { + type = types.nullOr types.str; + default = null; + example = literalExpression '' + SELECT name,password,gid FROM groups WHERE gid='%1$u' LIMIT 1 + ''; + description = '' + SQL query for the getgrgid + syscall. + ''; + }; + getgrent = mkOption { + type = types.nullOr types.str; + default = null; + example = literalExpression '' + SELECT name,password,gid FROM groups + ''; + description = '' + SQL query for the getgrent + syscall. + ''; + }; + memsbygid = mkOption { + type = types.nullOr types.str; + default = null; + example = literalExpression '' + SELECT username FROM grouplist WHERE gid='%1$u' + ''; + description = '' + SQL query for the memsbygid + syscall. + ''; + }; + gidsbymem = mkOption { + type = types.nullOr types.str; + default = null; + example = literalExpression '' + SELECT gid FROM grouplist WHERE username='%1$s' + ''; + description = '' + SQL query for the gidsbymem + syscall. + ''; + }; + }; + }; + }; + }; + }; + + config = mkIf cfg.enable { + system.nssModules = [ pkgs.libnss-mysql ]; + system.nssDatabases.shadow = [ "mysql" ]; + system.nssDatabases.group = [ "mysql" ]; + system.nssDatabases.passwd = [ "mysql" ]; + + environment.etc."security/pam_mysql.conf" = { + user = "root"; + group = "root"; + mode = "0600"; + # password will be added from password file in activation script + text = '' + users.host=${cfg.host} + users.db_user=${cfg.user} + users.database=${cfg.database} + users.table=${cfg.pam.table} + users.user_column=${cfg.pam.userColumn} + users.password_column=${cfg.pam.passwordColumn} + users.password_crypt=${cfg.pam.passwordCrypt} + users.disconnect_every_operation=${if cfg.pam.disconnectEveryOperation then "1" else "0"} + verbose=${if cfg.pam.verbose then "1" else "0"} + '' + optionalString (cfg.pam.cryptDefault != null) '' + users.use_${cfg.pam.cryptDefault}=1 + '' + optionalString (cfg.pam.where != null) '' + users.where_clause=${cfg.pam.where} + '' + optionalString (cfg.pam.statusColumn != null) '' + users.status_column=${cfg.pam.statusColumn} + '' + optionalString (cfg.pam.updateTable != null) '' + users.update_table=${cfg.pam.updateTable} + '' + optionalString cfg.pam.logging.enable '' + log.enabled=true + log.table=${cfg.pam.logging.table} + log.message_column=${cfg.pam.logging.msgColumn} + log.pid_column=${cfg.pam.logging.pidColumn} + log.user_column=${cfg.pam.logging.userColumn} + log.host_column=${cfg.pam.logging.hostColumn} + log.rhost_column=${cfg.pam.logging.rHostColumn} + log.time_column=${cfg.pam.logging.timeColumn} + ''; + }; + + environment.etc."libnss-mysql.cfg" = { + mode = "0600"; + user = config.services.nscd.user; + group = config.services.nscd.group; + text = optionalString (cfg.nss.getpwnam != null) '' + getpwnam ${cfg.nss.getpwnam} + '' + optionalString (cfg.nss.getpwuid != null) '' + getpwuid ${cfg.nss.getpwuid} + '' + optionalString (cfg.nss.getspnam != null) '' + getspnam ${cfg.nss.getspnam} + '' + optionalString (cfg.nss.getpwent != null) '' + getpwent ${cfg.nss.getpwent} + '' + optionalString (cfg.nss.getspent != null) '' + getspent ${cfg.nss.getspent} + '' + optionalString (cfg.nss.getgrnam != null) '' + getgrnam ${cfg.nss.getgrnam} + '' + optionalString (cfg.nss.getgrgid != null) '' + getgrgid ${cfg.nss.getgrgid} + '' + optionalString (cfg.nss.getgrent != null) '' + getgrent ${cfg.nss.getgrent} + '' + optionalString (cfg.nss.memsbygid != null) '' + memsbygid ${cfg.nss.memsbygid} + '' + optionalString (cfg.nss.gidsbymem != null) '' + gidsbymem ${cfg.nss.gidsbymem} + '' + '' + host ${cfg.host} + database ${cfg.database} + ''; + }; + + environment.etc."libnss-mysql-root.cfg" = { + mode = "0600"; + user = config.services.nscd.user; + group = config.services.nscd.group; + # password will be added from password file in activation script + text = '' + username ${cfg.user} + ''; + }; + + # Activation script to append the password from the password file + # to the configuration files. It also fixes the owner of the + # libnss-mysql-root.cfg because it is changed to root after the + # password is appended. + system.activationScripts.mysql-auth-passwords = '' + if [[ -r ${cfg.passwordFile} ]]; then + org_umask=$(umask) + umask 0077 + + conf_nss="$(mktemp)" + cp /etc/libnss-mysql-root.cfg $conf_nss + printf 'password %s\n' "$(cat ${cfg.passwordFile})" >> $conf_nss + mv -fT "$conf_nss" /etc/libnss-mysql-root.cfg + chown ${config.services.nscd.user}:${config.services.nscd.group} /etc/libnss-mysql-root.cfg + + conf_pam="$(mktemp)" + cp /etc/security/pam_mysql.conf $conf_pam + printf 'users.db_passwd=%s\n' "$(cat ${cfg.passwordFile})" >> $conf_pam + mv -fT "$conf_pam" /etc/security/pam_mysql.conf + + umask $org_umask + fi + ''; + }; +} diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 3010a213705..9c3c3fa9e34 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -22,6 +22,7 @@ ./config/ldap.nix ./config/locale.nix ./config/malloc.nix + ./config/mysql.nix ./config/networking.nix ./config/no-x-libs.nix ./config/nsswitch.nix diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix index d9d072b36e6..091af120dfd 100644 --- a/nixos/modules/security/pam.nix +++ b/nixos/modules/security/pam.nix @@ -142,6 +142,16 @@ let ''; }; + mysqlAuth = mkOption { + default = config.users.mysql.enable; + defaultText = literalExpression "config.users.mysql.enable"; + type = types.bool; + description = '' + If set, the pam_mysql module will be used to + authenticate users against a MySQL/MariaDB database. + ''; + }; + fprintAuth = mkOption { default = config.services.fprintd.enable; defaultText = literalExpression "config.services.fprintd.enable"; @@ -447,6 +457,9 @@ let optionalString use_ldap '' account sufficient ${pam_ldap}/lib/security/pam_ldap.so '' + + optionalString cfg.mysqlAuth '' + account sufficient ${pkgs.pam_mysql}/lib/security/pam_mysql.so config_file=/etc/security/pam_mysql.conf + '' + optionalString (config.services.sssd.enable && cfg.sssdStrictAccess==false) '' account sufficient ${pkgs.sssd}/lib/security/pam_sss.so '' + @@ -476,6 +489,9 @@ let optionalString cfg.logFailures '' auth required pam_faillock.so '' + + optionalString cfg.mysqlAuth '' + auth sufficient ${pkgs.pam_mysql}/lib/security/pam_mysql.so config_file=/etc/security/pam_mysql.conf + '' + optionalString (config.security.pam.enableSSHAgentAuth && cfg.sshAgentAuth) '' auth sufficient ${pkgs.pam_ssh_agent_auth}/libexec/pam_ssh_agent_auth.so file=${lib.concatStringsSep ":" config.services.openssh.authorizedKeysFiles} '' + @@ -573,6 +589,9 @@ let optionalString use_ldap '' password sufficient ${pam_ldap}/lib/security/pam_ldap.so '' + + optionalString cfg.mysqlAuth '' + password sufficient ${pkgs.pam_mysql}/lib/security/pam_mysql.so config_file=/etc/security/pam_mysql.conf + '' + optionalString config.services.sssd.enable '' password sufficient ${pkgs.sssd}/lib/security/pam_sss.so use_authtok '' + @@ -616,6 +635,9 @@ let optionalString use_ldap '' session optional ${pam_ldap}/lib/security/pam_ldap.so '' + + optionalString cfg.mysqlAuth '' + session optional ${pkgs.pam_mysql}/lib/security/pam_mysql.so config_file=/etc/security/pam_mysql.conf + '' + optionalString config.services.sssd.enable '' session optional ${pkgs.sssd}/lib/security/pam_sss.so '' + @@ -1241,6 +1263,9 @@ in optionalString (isEnabled (cfg: cfg.oathAuth)) '' "mr ${pkgs.oath-toolkit}/lib/security/pam_oath.so, '' + + optionalString (isEnabled (cfg: cfg.mysqlAuth)) '' + mr ${pkgs.pam_mysql}/lib/security/pam_mysql.so, + '' + optionalString (isEnabled (cfg: cfg.yubicoAuth)) '' mr ${pkgs.yubico-pam}/lib/security/pam_yubico.so, '' + diff --git a/nixos/modules/services/system/nscd.nix b/nixos/modules/services/system/nscd.nix index 9c98f851954..8bdb7ac3803 100644 --- a/nixos/modules/services/system/nscd.nix +++ b/nixos/modules/services/system/nscd.nix @@ -92,6 +92,9 @@ in config.environment.etc.hosts.source config.environment.etc."nsswitch.conf".source config.environment.etc."nscd.conf".source + ] ++ optionals config.users.mysql.enable [ + config.environment.etc."libnss-mysql.cfg".source + config.environment.etc."libnss-mysql-root.cfg".source ]; # In some configurations, nscd needs to be started as root; it will