diff options
author | Maximilian Bosch <maximilian@mbosch.me> | 2022-08-16 20:30:16 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-08-16 20:30:16 +0200 |
commit | 9e8ea1b855566d1d4cc8608d6bf291b53700d253 (patch) | |
tree | 8ad17c2db64f0a26be80c473ada2bd416c3cb2b2 /nixos/modules | |
parent | 5cd5de601e8375dae145938e66d5a300b644712f (diff) | |
parent | e23ace62686fb974353e86299c7003e089f323dd (diff) |
Merge pull request #183717 from NetaliDev/mysql-auth
nixos: add mysql/mariadb user authentication module
Diffstat (limited to 'nixos/modules')
-rw-r--r-- | nixos/modules/config/mysql.nix | 519 | ||||
-rw-r--r-- | nixos/modules/module-list.nix | 1 | ||||
-rw-r--r-- | nixos/modules/security/pam.nix | 30 | ||||
-rw-r--r-- | nixos/modules/services/system/nscd.nix | 48 |
4 files changed, 590 insertions, 8 deletions
diff --git a/nixos/modules/config/mysql.nix b/nixos/modules/config/mysql.nix new file mode 100644 index 0000000000000..8e7ce2a307e50 --- /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 <literal>pam_mysql</literal>"; + 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 <literal>table</literal> 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: + + <itemizedlist> + <listitem> + <para> + <literal>bit 0 (0x01)</literal>: + if flagged, <literal>pam_mysql</literal> deems the account to be expired and + returns <literal>PAM_ACCT_EXPIRED</literal>. That is, the account is supposed + to no longer be available. Note this doesn't mean that <literal>pam_mysql</literal> + rejects further authentication operations. + </para> + </listitem> + <listitem> + <para> + <literal>bit 1 (0x02)</literal>: + if flagged, <literal>pam_mysql</literal> deems the authentication token + (password) to be expired and returns <literal>PAM_NEW_AUTHTOK_REQD</literal>. + This ends up requiring that the user enter a new password. + </para> + </listitem> + </itemizedlist> + ''; + }; + 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: + + <itemizedlist> + <listitem> + <para> + <literal>0</literal> (or <literal>"plain"</literal>): + No encryption. Passwords are stored in plaintext. HIGHLY DISCOURAGED. + </para> + </listitem> + <listitem> + <para> + <literal>1</literal> (or <literal>"Y"</literal>): + Use crypt(3) function. + </para> + </listitem> + <listitem> + <para> + <literal>2</literal> (or <literal>"mysql"</literal>): + Use the MySQL PASSWORD() function. It is possible that the encryption function used + by <literal>pam_mysql</literal> is different from that of the MySQL server, as + <literal>pam_mysql</literal> uses the function defined in MySQL's C-client API + instead of using PASSWORD() SQL function in the query. + </para> + </listitem> + <listitem> + <para> + <literal>3</literal> (or <literal>"md5"</literal>): + Use plain hex MD5. + </para> + </listitem> + <listitem> + <para> + <literal>4</literal> (or <literal>"sha1"</literal>): + Use plain hex SHA1. + </para> + </listitem> + <listitem> + <para> + <literal>5</literal> (or <literal>"drupal7"</literal>): + Use Drupal7 salted passwords. + </para> + </listitem> + <listitem> + <para> + <literal>6</literal> (or <literal>"joomla15"</literal>): + Use Joomla15 salted passwords. + </para> + </listitem> + <listitem> + <para> + <literal>7</literal> (or <literal>"ssha"</literal>): + Use ssha hashed passwords. + </para> + </listitem> + <listitem> + <para> + <literal>8</literal> (or <literal>"sha512"</literal>): + Use sha512 hashed passwords. + </para> + </listitem> + <listitem> + <para> + <literal>9</literal> (or <literal>"sha256"</literal>): + Use sha256 hashed passwords. + </para> + </listitem> + </itemizedlist> + ''; + }; + cryptDefault = mkOption { + type = types.nullOr (types.enum [ "md5" "sha256" "sha512" "blowfish" ]); + default = null; + example = "blowfish"; + description = "The default encryption method to use for <literal>passwordCrypt = 1</literal>."; + }; + 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 + <literal>pam_mysql</literal> is doing. May be useful for debugging. + ''; + }; + disconnectEveryOperation = mkOption { + type = types.bool; + default = false; + description = '' + By default, <literal>pam_mysql</literal> 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 <literal>pam_mysql's</literal> 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 <literal>pam_set_item(PAM_RHOST) + </literal>. + ''; + }; + 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 <literal>libnss-mysql</literal>. + + All examples are from the <link xlink:href="https://github.com/saknopper/libnss-mysql/tree/master/sample/minimal">minimal example</link> + of <literal>libnss-mysql</literal>, 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 <link + xlink:href="https://man7.org/linux/man-pages/man3/getpwnam.3.html">getpwnam</link> + 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 <link + xlink:href="https://man7.org/linux/man-pages/man3/getpwuid.3.html">getpwuid</link> + 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 <link + xlink:href="https://man7.org/linux/man-pages/man3/getspnam.3.html">getspnam</link> + 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 <link + xlink:href="https://man7.org/linux/man-pages/man3/getpwent.3.html">getpwent</link> + 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 <link + xlink:href="https://man7.org/linux/man-pages/man3/getspent.3.html">getspent</link> + 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 <link + xlink:href="https://man7.org/linux/man-pages/man3/getgrnam.3.html">getgrnam</link> + 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 <link + xlink:href="https://man7.org/linux/man-pages/man3/getgrgid.3.html">getgrgid</link> + syscall. + ''; + }; + getgrent = mkOption { + type = types.nullOr types.str; + default = null; + example = literalExpression '' + SELECT name,password,gid FROM groups + ''; + description = '' + SQL query for the <link + xlink:href="https://man7.org/linux/man-pages/man3/getgrent.3.html">getgrent</link> + 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 <link + xlink:href="https://man7.org/linux/man-pages/man3/memsbygid.3.html">memsbygid</link> + 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 <link + xlink:href="https://man7.org/linux/man-pages/man3/gidsbymem.3.html">gidsbymem</link> + 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 c1f435ec569c9..3a849fcfec77f 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 9a1acba00d0ea..9f89508bd1fe5 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 <literal>pam_mysql</literal> 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 c3046a5b4cf48..aabcae3b67364 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"; |