diff options
Diffstat (limited to 'nixpkgs/nixos/modules/services/databases/mysql.nix')
-rw-r--r-- | nixpkgs/nixos/modules/services/databases/mysql.nix | 294 |
1 files changed, 165 insertions, 129 deletions
diff --git a/nixpkgs/nixos/modules/services/databases/mysql.nix b/nixpkgs/nixos/modules/services/databases/mysql.nix index 51885881cf7..7d0a3f9afc4 100644 --- a/nixpkgs/nixos/modules/services/databases/mysql.nix +++ b/nixpkgs/nixos/modules/services/databases/mysql.nix @@ -6,12 +6,10 @@ let cfg = config.services.mysql; - mysql = cfg.package; - - isMariaDB = lib.getName mysql == lib.getName pkgs.mariadb; + isMariaDB = lib.getName cfg.package == lib.getName pkgs.mariadb; mysqldOptions = - "--user=${cfg.user} --datadir=${cfg.dataDir} --basedir=${mysql}"; + "--user=${cfg.user} --datadir=${cfg.dataDir} --basedir=${cfg.package}"; settingsFile = pkgs.writeText "my.cnf" ( generators.toINI { listsAsDuplicateKeys = true; } cfg.settings + @@ -22,7 +20,7 @@ in { imports = [ - (mkRemovedOptionModule [ "services" "mysql" "pidDir" ] "Don't wait for pidfiles, describe dependencies through systemd") + (mkRemovedOptionModule [ "services" "mysql" "pidDir" ] "Don't wait for pidfiles, describe dependencies through systemd.") (mkRemovedOptionModule [ "services" "mysql" "rootPassword" ] "Use socket authentication or set the password outside of the nix store.") ]; @@ -46,25 +44,31 @@ in type = types.nullOr types.str; default = null; example = literalExample "0.0.0.0"; - description = "Address to bind to. The default is to bind to all addresses"; + description = "Address to bind to. The default is to bind to all addresses."; }; port = mkOption { type = types.int; default = 3306; - description = "Port of MySQL"; + description = "Port of MySQL."; }; user = mkOption { type = types.str; default = "mysql"; - description = "User account under which MySQL runs"; + description = "User account under which MySQL runs."; + }; + + group = mkOption { + type = types.str; + default = "mysql"; + description = "Group under which MySQL runs."; }; dataDir = mkOption { type = types.path; example = "/var/lib/mysql"; - description = "Location where MySQL stores its table files"; + description = "Location where MySQL stores its table files."; }; configFile = mkOption { @@ -171,7 +175,7 @@ in initialScript = mkOption { type = types.nullOr types.path; default = null; - description = "A file containing SQL statements to be executed on the first startup. Can be used for granting certain permissions on the database"; + description = "A file containing SQL statements to be executed on the first startup. Can be used for granting certain permissions on the database."; }; ensureDatabases = mkOption { @@ -259,33 +263,33 @@ in serverId = mkOption { type = types.int; default = 1; - description = "Id of the MySQL server instance. This number must be unique for each instance"; + description = "Id of the MySQL server instance. This number must be unique for each instance."; }; masterHost = mkOption { type = types.str; - description = "Hostname of the MySQL master server"; + description = "Hostname of the MySQL master server."; }; slaveHost = mkOption { type = types.str; - description = "Hostname of the MySQL slave server"; + description = "Hostname of the MySQL slave server."; }; masterUser = mkOption { type = types.str; - description = "Username of the MySQL replication user"; + description = "Username of the MySQL replication user."; }; masterPassword = mkOption { type = types.str; - description = "Password of the MySQL replication user"; + description = "Password of the MySQL replication user."; }; masterPort = mkOption { type = types.int; default = 3306; - description = "Port number on which the MySQL master server runs"; + description = "Port number on which the MySQL master server runs."; }; }; }; @@ -317,28 +321,33 @@ in binlog-ignore-db = [ "information_schema" "performance_schema" "mysql" ]; }) (mkIf (!isMariaDB) { - plugin-load-add = optional (cfg.ensureUsers != []) "auth_socket.so"; + plugin-load-add = "auth_socket.so"; }) ]; - users.users.mysql = { - description = "MySQL server user"; - group = "mysql"; - uid = config.ids.uids.mysql; + users.users = optionalAttrs (cfg.user == "mysql") { + mysql = { + description = "MySQL server user"; + group = cfg.group; + uid = config.ids.uids.mysql; + }; }; - users.groups.mysql.gid = config.ids.gids.mysql; + users.groups = optionalAttrs (cfg.group == "mysql") { + mysql.gid = config.ids.gids.mysql; + }; - environment.systemPackages = [mysql]; + environment.systemPackages = [ cfg.package ]; environment.etc."my.cnf".source = cfg.configFile; systemd.tmpfiles.rules = [ - "d '${cfg.dataDir}' 0700 ${cfg.user} mysql -" + "d '${cfg.dataDir}' 0700 '${cfg.user}' '${cfg.group}' - -" + "z '${cfg.dataDir}' 0700 '${cfg.user}' '${cfg.group}' - -" ]; systemd.services.mysql = let - hasNotify = (cfg.package == pkgs.mariadb); + hasNotify = isMariaDB; in { description = "MySQL Server"; @@ -356,126 +365,153 @@ in preStart = if isMariaDB then '' if ! test -e ${cfg.dataDir}/mysql; then - ${mysql}/bin/mysql_install_db --defaults-file=/etc/my.cnf ${mysqldOptions} - touch /tmp/mysql_init + ${cfg.package}/bin/mysql_install_db --defaults-file=/etc/my.cnf ${mysqldOptions} + touch ${cfg.dataDir}/mysql_init fi '' else '' if ! test -e ${cfg.dataDir}/mysql; then - ${mysql}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} --initialize-insecure - touch /tmp/mysql_init + ${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} --initialize-insecure + touch ${cfg.dataDir}/mysql_init fi ''; - serviceConfig = { - User = cfg.user; - Group = "mysql"; - Type = if hasNotify then "notify" else "simple"; - RuntimeDirectory = "mysqld"; - RuntimeDirectoryMode = "0755"; - Restart = "on-abort"; - RestartSec = "5s"; - # The last two environment variables are used for starting Galera clusters - ExecStart = "${mysql}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} $_WSREP_NEW_CLUSTER $_WSREP_START_POSITION"; - ExecStartPost = - let - setupScript = pkgs.writeScript "mysql-setup" '' - #!${pkgs.runtimeShell} -e - - ${optionalString (!hasNotify) '' - # Wait until the MySQL server is available for use - count=0 - while [ ! -e /run/mysqld/mysqld.sock ] - do - if [ $count -eq 30 ] - then - echo "Tried 30 times, giving up..." - exit 1 - fi - - echo "MySQL daemon not yet started. Waiting for 1 second..." - count=$((count++)) - sleep 1 - done - ''} - - if [ -f /tmp/mysql_init ] + postStart = let + # The super user account to use on *first* run of MySQL server + superUser = if isMariaDB then cfg.user else "root"; + in '' + ${optionalString (!hasNotify) '' + # Wait until the MySQL server is available for use + count=0 + while [ ! -e /run/mysqld/mysqld.sock ] + do + if [ $count -eq 30 ] then - ${concatMapStrings (database: '' - # Create initial databases - if ! test -e "${cfg.dataDir}/${database.name}"; then - echo "Creating initial database: ${database.name}" - ( echo 'create database `${database.name}`;' - - ${optionalString (database.schema != null) '' - echo 'use `${database.name}`;' - - # TODO: this silently falls through if database.schema does not exist, - # we should catch this somehow and exit, but can't do it here because we're in a subshell. - if [ -f "${database.schema}" ] - then - cat ${database.schema} - elif [ -d "${database.schema}" ] - then - cat ${database.schema}/mysql-databases/*.sql - fi - ''} - ) | ${mysql}/bin/mysql -u root -N - fi - '') cfg.initialDatabases} - - ${optionalString (cfg.replication.role == "master") - '' - # Set up the replication master + echo "Tried 30 times, giving up..." + exit 1 + fi - ( echo "use mysql;" - echo "CREATE USER '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' IDENTIFIED WITH mysql_native_password;" - echo "SET PASSWORD FOR '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' = PASSWORD('${cfg.replication.masterPassword}');" - echo "GRANT REPLICATION SLAVE ON *.* TO '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}';" - ) | ${mysql}/bin/mysql -u root -N + echo "MySQL daemon not yet started. Waiting for 1 second..." + count=$((count++)) + sleep 1 + done + ''} + + if [ -f ${cfg.dataDir}/mysql_init ] + then + # While MariaDB comes with a 'mysql' super user account since 10.4.x, MySQL does not + # Since we don't want to run this service as 'root' we need to ensure the account exists on first run + ( echo "CREATE USER IF NOT EXISTS '${cfg.user}'@'localhost' IDENTIFIED WITH ${if isMariaDB then "unix_socket" else "auth_socket"};" + echo "GRANT ALL PRIVILEGES ON *.* TO '${cfg.user}'@'localhost' WITH GRANT OPTION;" + ) | ${cfg.package}/bin/mysql -u ${superUser} -N + + ${concatMapStrings (database: '' + # Create initial databases + if ! test -e "${cfg.dataDir}/${database.name}"; then + echo "Creating initial database: ${database.name}" + ( echo 'create database `${database.name}`;' + + ${optionalString (database.schema != null) '' + echo 'use `${database.name}`;' + + # TODO: this silently falls through if database.schema does not exist, + # we should catch this somehow and exit, but can't do it here because we're in a subshell. + if [ -f "${database.schema}" ] + then + cat ${database.schema} + elif [ -d "${database.schema}" ] + then + cat ${database.schema}/mysql-databases/*.sql + fi ''} + ) | ${cfg.package}/bin/mysql -u ${superUser} -N + fi + '') cfg.initialDatabases} - ${optionalString (cfg.replication.role == "slave") - '' - # Set up the replication slave + ${optionalString (cfg.replication.role == "master") + '' + # Set up the replication master - ( echo "stop slave;" - echo "change master to master_host='${cfg.replication.masterHost}', master_user='${cfg.replication.masterUser}', master_password='${cfg.replication.masterPassword}';" - echo "start slave;" - ) | ${mysql}/bin/mysql -u root -N - ''} + ( echo "use mysql;" + echo "CREATE USER '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' IDENTIFIED WITH mysql_native_password;" + echo "SET PASSWORD FOR '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' = PASSWORD('${cfg.replication.masterPassword}');" + echo "GRANT REPLICATION SLAVE ON *.* TO '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}';" + ) | ${cfg.package}/bin/mysql -u ${superUser} -N + ''} - ${optionalString (cfg.initialScript != null) - '' - # Execute initial script - # using toString to avoid copying the file to nix store if given as path instead of string, - # as it might contain credentials - cat ${toString cfg.initialScript} | ${mysql}/bin/mysql -u root -N - ''} + ${optionalString (cfg.replication.role == "slave") + '' + # Set up the replication slave - rm /tmp/mysql_init - fi + ( echo "stop slave;" + echo "change master to master_host='${cfg.replication.masterHost}', master_user='${cfg.replication.masterUser}', master_password='${cfg.replication.masterPassword}';" + echo "start slave;" + ) | ${cfg.package}/bin/mysql -u ${superUser} -N + ''} - ${optionalString (cfg.ensureDatabases != []) '' - ( - ${concatMapStrings (database: '' - echo "CREATE DATABASE IF NOT EXISTS \`${database}\`;" - '') cfg.ensureDatabases} - ) | ${mysql}/bin/mysql -u root -N + ${optionalString (cfg.initialScript != null) + '' + # Execute initial script + # using toString to avoid copying the file to nix store if given as path instead of string, + # as it might contain credentials + cat ${toString cfg.initialScript} | ${cfg.package}/bin/mysql -u ${superUser} -N ''} - ${concatMapStrings (user: - '' - ( echo "CREATE USER IF NOT EXISTS '${user.name}'@'localhost' IDENTIFIED WITH ${if isMariaDB then "unix_socket" else "auth_socket"};" - ${concatStringsSep "\n" (mapAttrsToList (database: permission: '' - echo "GRANT ${permission} ON ${database} TO '${user.name}'@'localhost';" - '') user.ensurePermissions)} - ) | ${mysql}/bin/mysql -u root -N - '') cfg.ensureUsers} - ''; - in - # ensureDatbases & ensureUsers depends on this script being run as root - # when the user has secured their mysql install - "+${setupScript}"; + rm ${cfg.dataDir}/mysql_init + fi + + ${optionalString (cfg.ensureDatabases != []) '' + ( + ${concatMapStrings (database: '' + echo "CREATE DATABASE IF NOT EXISTS \`${database}\`;" + '') cfg.ensureDatabases} + ) | ${cfg.package}/bin/mysql -N + ''} + + ${concatMapStrings (user: + '' + ( echo "CREATE USER IF NOT EXISTS '${user.name}'@'localhost' IDENTIFIED WITH ${if isMariaDB then "unix_socket" else "auth_socket"};" + ${concatStringsSep "\n" (mapAttrsToList (database: permission: '' + echo "GRANT ${permission} ON ${database} TO '${user.name}'@'localhost';" + '') user.ensurePermissions)} + ) | ${cfg.package}/bin/mysql -N + '') cfg.ensureUsers} + ''; + + serviceConfig = { + Type = if hasNotify then "notify" else "simple"; + Restart = "on-abort"; + RestartSec = "5s"; + # The last two environment variables are used for starting Galera clusters + ExecStart = "${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} $_WSREP_NEW_CLUSTER $_WSREP_START_POSITION"; + # User and group + User = cfg.user; + Group = cfg.group; + # Runtime directory and mode + RuntimeDirectory = "mysqld"; + RuntimeDirectoryMode = "0755"; + # Access write directories + ReadWritePaths = [ cfg.dataDir ]; + # Capabilities + CapabilityBoundingSet = ""; + # Security + NoNewPrivileges = true; + # Sandboxing + ProtectSystem = "strict"; + ProtectHome = true; + PrivateTmp = true; + PrivateDevices = true; + ProtectHostname = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; + LockPersonality = true; + MemoryDenyWriteExecute = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + PrivateMounts = true; + # System Call Filtering + SystemCallArchitectures = "native"; }; }; |