diff --git a/nixos/modules/config/mysql.nix b/nixos/modules/config/mysql.nix
new file mode 100644
index 000000000000..8e7ce2a307e5
--- /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 c1f435ec569c..3a849fcfec77 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 9a1acba00d0e..9f89508bd1fe 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";
@@ -441,11 +451,13 @@ let
(
''
# Account management.
- account required pam_unix.so
'' +
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
'' +
@@ -459,7 +471,11 @@ let
account [success=ok ignore=ignore default=die] ${pkgs.google-guest-oslogin}/lib/security/pam_oslogin_login.so
account [success=ok default=ignore] ${pkgs.google-guest-oslogin}/lib/security/pam_oslogin_admin.so
'' +
+ # The required pam_unix.so module has to come after all the sufficient modules
+ # because otherwise, the account lookup will fail if the user does not exist
+ # locally, for example with MySQL- or LDAP-auth.
''
+ account required pam_unix.so
# Authentication management.
'' +
@@ -475,6 +491,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}
'' +
@@ -572,6 +591,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
'' +
@@ -615,6 +637,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
'' +
@@ -1236,6 +1261,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 c3046a5b4cf4..aabcae3b6736 100644
--- a/nixos/modules/services/system/nscd.nix
+++ b/nixos/modules/services/system/nscd.nix
@@ -27,6 +27,22 @@ in
'';
};
+ user = mkOption {
+ type = types.str;
+ default = "nscd";
+ description = ''
+ User account under which nscd runs.
+ '';
+ };
+
+ group = mkOption {
+ type = types.str;
+ default = "nscd";
+ description = ''
+ User group under which nscd runs.
+ '';
+ };
+
config = mkOption {
type = types.lines;
default = builtins.readFile ./nscd.conf;
@@ -56,6 +72,13 @@ in
config = mkIf cfg.enable {
environment.etc."nscd.conf".text = cfg.config;
+ users.users.${cfg.user} = {
+ isSystemUser = true;
+ group = cfg.group;
+ };
+
+ users.groups.${cfg.group} = {};
+
systemd.services.nscd =
{ description = "Name Service Cache Daemon";
@@ -69,18 +92,29 @@ 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
];
- # We use DynamicUser because in default configurations nscd doesn't
- # create any files that need to survive restarts. However, in some
- # configurations, nscd needs to be started as root; it will drop
- # privileges after all the NSS modules have read their configuration
- # files. So prefix the ExecStart command with "!" to prevent systemd
- # from dropping privileges early. See ExecStart in systemd.service(5).
+ # In some configurations, nscd needs to be started as root; it will
+ # drop privileges after all the NSS modules have read their
+ # configuration files. So prefix the ExecStart command with "!" to
+ # prevent systemd from dropping privileges early. See ExecStart in
+ # systemd.service(5). We use a static user, because some NSS modules
+ # sill want to read their configuration files after the privilege drop
+ # and so users can set the owner of those files to the nscd user.
serviceConfig =
{ ExecStart = "!@${cfg.package}/bin/nscd nscd";
Type = "forking";
- DynamicUser = true;
+ User = cfg.user;
+ Group = cfg.group;
+ RemoveIPC = true;
+ PrivateTmp = true;
+ NoNewPrivileges = true;
+ RestrictSUIDSGID = true;
+ ProtectSystem = "strict";
+ ProtectHome = "read-only";
RuntimeDirectory = "nscd";
PIDFile = "/run/nscd/nscd.pid";
Restart = "always";
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index a0af42b0056a..43fb4d2bd238 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -41,6 +41,7 @@ in {
apparmor = handleTest ./apparmor.nix {};
atd = handleTest ./atd.nix {};
atop = handleTest ./atop.nix {};
+ auth-mysql = handleTest ./auth-mysql.nix {};
avahi = handleTest ./avahi.nix {};
avahi-with-resolved = handleTest ./avahi.nix { networkd = true; };
babeld = handleTest ./babeld.nix {};
diff --git a/nixos/tests/auth-mysql.nix b/nixos/tests/auth-mysql.nix
new file mode 100644
index 000000000000..0ed4b050a69a
--- /dev/null
+++ b/nixos/tests/auth-mysql.nix
@@ -0,0 +1,177 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+
+let
+ dbUser = "nixos_auth";
+ dbPassword = "topsecret123";
+ dbName = "auth";
+
+ mysqlUsername = "mysqltest";
+ mysqlPassword = "topsecretmysqluserpassword123";
+ mysqlGroup = "mysqlusers";
+
+ localUsername = "localtest";
+ localPassword = "topsecretlocaluserpassword123";
+
+ mysqlInit = pkgs.writeText "mysqlInit" ''
+ CREATE USER '${dbUser}'@'localhost' IDENTIFIED BY '${dbPassword}';
+ CREATE DATABASE ${dbName};
+ GRANT ALL PRIVILEGES ON ${dbName}.* TO '${dbUser}'@'localhost';
+ FLUSH PRIVILEGES;
+
+ USE ${dbName};
+ CREATE TABLE `groups` (
+ rowid int(11) NOT NULL auto_increment,
+ gid int(11) NOT NULL,
+ name char(255) NOT NULL,
+ PRIMARY KEY (rowid)
+ );
+
+ CREATE TABLE `users` (
+ name varchar(255) NOT NULL,
+ uid int(11) NOT NULL auto_increment,
+ gid int(11) NOT NULL,
+ password varchar(255) NOT NULL,
+ PRIMARY KEY (uid),
+ UNIQUE (name)
+ ) AUTO_INCREMENT=5000;
+
+ INSERT INTO `users` (name, uid, gid, password) VALUES
+ ('${mysqlUsername}', 5000, 5000, SHA2('${mysqlPassword}', 256));
+ INSERT INTO `groups` (name, gid) VALUES ('${mysqlGroup}', 5000);
+ '';
+in
+{
+ name = "auth-mysql";
+ meta.maintainers = with lib.maintainers; [ netali ];
+
+ nodes.machine =
+ { ... }:
+ {
+ services.mysql = {
+ enable = true;
+ package = pkgs.mariadb;
+ settings.mysqld.bind-address = "127.0.0.1";
+ initialScript = mysqlInit;
+ };
+
+ users.users.${localUsername} = {
+ isNormalUser = true;
+ password = localPassword;
+ };
+
+ security.pam.services.login.makeHomeDir = true;
+
+ users.mysql = {
+ enable = true;
+ host = "127.0.0.1";
+ user = dbUser;
+ database = dbName;
+ passwordFile = "${builtins.toFile "dbPassword" dbPassword}";
+ pam = {
+ table = "users";
+ userColumn = "name";
+ passwordColumn = "password";
+ passwordCrypt = "sha256";
+ disconnectEveryOperation = true;
+ };
+ nss = {
+ getpwnam = ''
+ SELECT name, 'x', uid, gid, name, CONCAT('/home/', name), "/run/current-system/sw/bin/bash" \
+ FROM users \
+ WHERE name='%1$s' \
+ LIMIT 1
+ '';
+ getpwuid = ''
+ SELECT name, 'x', uid, gid, name, CONCAT('/home/', name), "/run/current-system/sw/bin/bash" \
+ FROM users \
+ WHERE id=%1$u \
+ LIMIT 1
+ '';
+ getspnam = ''
+ SELECT name, password, 1, 0, 99999, 7, 0, -1, 0 \
+ FROM users \
+ WHERE name='%1$s' \
+ LIMIT 1
+ '';
+ getpwent = ''
+ SELECT name, 'x', uid, gid, name, CONCAT('/home/', name), "/run/current-system/sw/bin/bash" \
+ FROM users
+ '';
+ getspent = ''
+ SELECT name, password, 1, 0, 99999, 7, 0, -1, 0 \
+ FROM users
+ '';
+ getgrnam = ''
+ SELECT name, 'x', gid FROM groups WHERE name='%1$s' LIMIT 1
+ '';
+ getgrgid = ''
+ SELECT name, 'x', gid FROM groups WHERE gid='%1$u' LIMIT 1
+ '';
+ getgrent = ''
+ SELECT name, 'x', gid FROM groups
+ '';
+ memsbygid = ''
+ SELECT name FROM users WHERE gid=%1$u
+ '';
+ gidsbymem = ''
+ SELECT gid FROM users WHERE name='%1$s'
+ '';
+ };
+ };
+ };
+
+ testScript = ''
+ def switch_to_tty(tty_number):
+ machine.fail(f"pgrep -f 'agetty.*tty{tty_number}'")
+ machine.send_key(f"alt-f{tty_number}")
+ machine.wait_until_succeeds(f"[ $(fgconsole) = {tty_number} ]")
+ machine.wait_for_unit(f"getty@tty{tty_number}.service")
+ machine.wait_until_succeeds(f"pgrep -f 'agetty.*tty{tty_number}'")
+
+
+ def try_login(tty_number, username, password):
+ machine.wait_until_tty_matches(tty_number, "login: ")
+ machine.send_chars(f"{username}\n")
+ machine.wait_until_tty_matches(tty_number, f"login: {username}")
+ machine.wait_until_succeeds("pgrep login")
+ machine.wait_until_tty_matches(tty_number, "Password: ")
+ machine.send_chars(f"{password}\n")
+
+
+ machine.wait_for_unit("multi-user.target")
+ machine.wait_for_unit("mysql.service")
+ machine.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
+
+ with subtest("Local login"):
+ switch_to_tty("2")
+ try_login("2", "${localUsername}", "${localPassword}")
+
+ machine.wait_until_succeeds("pgrep -u ${localUsername} bash")
+ machine.send_chars("id > local_id.txt\n")
+ machine.wait_for_file("/home/${localUsername}/local_id.txt")
+ machine.succeed("cat /home/${localUsername}/local_id.txt | grep 'uid=1000(${localUsername}) gid=100(users) groups=100(users)'")
+
+ with subtest("Local incorrect login"):
+ switch_to_tty("3")
+ try_login("3", "${localUsername}", "wrongpassword")
+
+ machine.wait_until_tty_matches("3", "Login incorrect")
+ machine.wait_until_tty_matches("3", "login:")
+
+ with subtest("MySQL login"):
+ switch_to_tty("4")
+ try_login("4", "${mysqlUsername}", "${mysqlPassword}")
+
+ machine.wait_until_succeeds("pgrep -u ${mysqlUsername} bash")
+ machine.send_chars("id > mysql_id.txt\n")
+ machine.wait_for_file("/home/${mysqlUsername}/mysql_id.txt")
+ machine.succeed("cat /home/${mysqlUsername}/mysql_id.txt | grep 'uid=5000(${mysqlUsername}) gid=5000(${mysqlGroup}) groups=5000(${mysqlGroup})'")
+
+ with subtest("MySQL incorrect login"):
+ switch_to_tty("5")
+ try_login("5", "${mysqlUsername}", "wrongpassword")
+
+ machine.wait_until_tty_matches("5", "Login incorrect")
+ machine.wait_until_tty_matches("5", "login:")
+ '';
+})
diff --git a/pkgs/os-specific/linux/libnss-mysql/default.nix b/pkgs/os-specific/linux/libnss-mysql/default.nix
new file mode 100644
index 000000000000..77e629b03074
--- /dev/null
+++ b/pkgs/os-specific/linux/libnss-mysql/default.nix
@@ -0,0 +1,30 @@
+{ lib, stdenv, fetchFromGitHub, autoreconfHook, which, libmysqlclient }:
+
+stdenv.mkDerivation rec {
+ pname = "libnss-mysql";
+ version = "1.7.1";
+
+ src = fetchFromGitHub {
+ owner = "saknopper";
+ repo = "libnss-mysql";
+ rev = "v${version}";
+ sha256 = "1fhsswa3h2nkhjkyjxxqnj07rlx6bmfvd8j521snimx2jba8h0d6";
+ };
+
+ nativeBuildInputs = [ autoreconfHook which ];
+ buildInputs = [ libmysqlclient ];
+
+ configureFlags = [ "--sysconfdir=/etc" ];
+ installFlags = [ "sysconfdir=$(out)/etc" ];
+ postInstall = ''
+ rm -r $out/etc
+ '';
+
+ meta = with lib; {
+ description = "MySQL module for the Solaris Nameservice Switch (NSS)";
+ homepage = "https://github.com/saknopper/libnss-mysql";
+ license = licenses.gpl2Plus;
+ platforms = platforms.linux;
+ maintainers = with maintainers; [ netali ];
+ };
+}
diff --git a/pkgs/os-specific/linux/pam_mysql/default.nix b/pkgs/os-specific/linux/pam_mysql/default.nix
new file mode 100644
index 000000000000..807899cf2b28
--- /dev/null
+++ b/pkgs/os-specific/linux/pam_mysql/default.nix
@@ -0,0 +1,24 @@
+{ lib, stdenv, fetchFromGitHub, meson, ninja, pam, pkg-config, libmysqlclient, mariadb }:
+
+stdenv.mkDerivation rec {
+ pname = "pam_mysql";
+ version = "1.0.0-beta2";
+
+ src = fetchFromGitHub {
+ owner = "NigelCunningham";
+ repo = "pam-MySQL";
+ rev = version;
+ sha256 = "07acf0hbhkd0kg49gnj4nb5ilnv3v4xx3dsggvzvjg8gi3cjmsap";
+ };
+
+ nativeBuildInputs = [ meson pkg-config ninja ];
+ buildInputs = [ pam libmysqlclient mariadb ];
+
+ meta = with lib; {
+ description = "PAM authentication module against a MySQL database";
+ homepage = "https://github.com/NigelCunningham/pam-MySQL";
+ license = licenses.gpl2Plus;
+ platforms = platforms.linux;
+ maintainers = with maintainers; [ netali ];
+ };
+}
diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix
index 39d7da273505..eda2b3fd9ff8 100644
--- a/pkgs/top-level/all-packages.nix
+++ b/pkgs/top-level/all-packages.nix
@@ -4144,6 +4144,8 @@ with pkgs;
libndtypes = callPackage ../development/libraries/libndtypes { };
+ libnss-mysql = callPackage ../os-specific/linux/libnss-mysql { };
+
libxnd = callPackage ../development/libraries/libxnd { };
lifeograph = callPackage ../applications/editors/lifeograph { };
@@ -24378,6 +24380,8 @@ with pkgs;
pam_mount = callPackage ../os-specific/linux/pam_mount { };
+ pam_mysql = callPackage ../os-specific/linux/pam_mysql { };
+
pam_p11 = callPackage ../os-specific/linux/pam_p11 { };
pam_pgsql = callPackage ../os-specific/linux/pam_pgsql { };