about summary refs log tree commit diff
path: root/nixos/modules
diff options
context:
space:
mode:
authorMaximilian Bosch <maximilian@mbosch.me>2022-08-16 20:30:16 +0200
committerGitHub <noreply@github.com>2022-08-16 20:30:16 +0200
commit9e8ea1b855566d1d4cc8608d6bf291b53700d253 (patch)
tree8ad17c2db64f0a26be80c473ada2bd416c3cb2b2 /nixos/modules
parent5cd5de601e8375dae145938e66d5a300b644712f (diff)
parente23ace62686fb974353e86299c7003e089f323dd (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.nix519
-rw-r--r--nixos/modules/module-list.nix1
-rw-r--r--nixos/modules/security/pam.nix30
-rw-r--r--nixos/modules/services/system/nscd.nix48
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";