Merge pull request #95231 from aanderse/mysql-cleanup
nixos/mysql: run postStart as an unprivileged user
This commit is contained in:
commit
f1f4cc6e1b
@ -114,6 +114,17 @@ systemd.services.mysql.serviceConfig.ProtectHome = lib.mkForce "read-only";
|
|||||||
systemd.services.mysql.serviceConfig.ReadWritePaths = [ "/var/data" ];
|
systemd.services.mysql.serviceConfig.ReadWritePaths = [ "/var/data" ];
|
||||||
</programlisting>
|
</programlisting>
|
||||||
</para>
|
</para>
|
||||||
|
<para>
|
||||||
|
The MySQL service no longer runs its <literal>systemd</literal> service startup script as <literal>root</literal> anymore. A dedicated non <literal>root</literal>
|
||||||
|
super user account is required for operation. This means users with an existing MySQL or MariaDB database server are required to run the following SQL statements
|
||||||
|
as a super admin user before upgrading:
|
||||||
|
<programlisting>
|
||||||
|
CREATE USER IF NOT EXISTS 'mysql'@'localhost' identified with unix_socket;
|
||||||
|
GRANT ALL PRIVILEGES ON *.* TO 'mysql'@'localhost' WITH GRANT OPTION;
|
||||||
|
</programlisting>
|
||||||
|
If you use MySQL instead of MariaDB please replace <literal>unix_socket</literal> with <literal>auth_socket</literal>. If you have changed the value of <xref linkend="opt-services.mysql.user"/>
|
||||||
|
from the default of <literal>mysql</literal> to a different user please change <literal>'mysql'@'localhost'</literal> to the corresponding user instead.
|
||||||
|
</para>
|
||||||
</listitem>
|
</listitem>
|
||||||
<listitem>
|
<listitem>
|
||||||
<para>
|
<para>
|
||||||
|
@ -6,12 +6,10 @@ let
|
|||||||
|
|
||||||
cfg = config.services.mysql;
|
cfg = config.services.mysql;
|
||||||
|
|
||||||
mysql = cfg.package;
|
isMariaDB = lib.getName cfg.package == lib.getName pkgs.mariadb;
|
||||||
|
|
||||||
isMariaDB = lib.getName mysql == lib.getName pkgs.mariadb;
|
|
||||||
|
|
||||||
mysqldOptions =
|
mysqldOptions =
|
||||||
"--user=${cfg.user} --datadir=${cfg.dataDir} --basedir=${mysql}";
|
"--user=${cfg.user} --datadir=${cfg.dataDir} --basedir=${cfg.package}";
|
||||||
|
|
||||||
settingsFile = pkgs.writeText "my.cnf" (
|
settingsFile = pkgs.writeText "my.cnf" (
|
||||||
generators.toINI { listsAsDuplicateKeys = true; } cfg.settings +
|
generators.toINI { listsAsDuplicateKeys = true; } cfg.settings +
|
||||||
@ -22,7 +20,7 @@ in
|
|||||||
|
|
||||||
{
|
{
|
||||||
imports = [
|
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.")
|
(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;
|
type = types.nullOr types.str;
|
||||||
default = null;
|
default = null;
|
||||||
example = literalExample "0.0.0.0";
|
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 {
|
port = mkOption {
|
||||||
type = types.int;
|
type = types.int;
|
||||||
default = 3306;
|
default = 3306;
|
||||||
description = "Port of MySQL";
|
description = "Port of MySQL.";
|
||||||
};
|
};
|
||||||
|
|
||||||
user = mkOption {
|
user = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = "mysql";
|
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 {
|
dataDir = mkOption {
|
||||||
type = types.path;
|
type = types.path;
|
||||||
example = "/var/lib/mysql";
|
example = "/var/lib/mysql";
|
||||||
description = "Location where MySQL stores its table files";
|
description = "Location where MySQL stores its table files.";
|
||||||
};
|
};
|
||||||
|
|
||||||
configFile = mkOption {
|
configFile = mkOption {
|
||||||
@ -171,7 +175,7 @@ in
|
|||||||
initialScript = mkOption {
|
initialScript = mkOption {
|
||||||
type = types.nullOr types.path;
|
type = types.nullOr types.path;
|
||||||
default = null;
|
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 {
|
ensureDatabases = mkOption {
|
||||||
@ -259,33 +263,33 @@ in
|
|||||||
serverId = mkOption {
|
serverId = mkOption {
|
||||||
type = types.int;
|
type = types.int;
|
||||||
default = 1;
|
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 {
|
masterHost = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
description = "Hostname of the MySQL master server";
|
description = "Hostname of the MySQL master server.";
|
||||||
};
|
};
|
||||||
|
|
||||||
slaveHost = mkOption {
|
slaveHost = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
description = "Hostname of the MySQL slave server";
|
description = "Hostname of the MySQL slave server.";
|
||||||
};
|
};
|
||||||
|
|
||||||
masterUser = mkOption {
|
masterUser = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
description = "Username of the MySQL replication user";
|
description = "Username of the MySQL replication user.";
|
||||||
};
|
};
|
||||||
|
|
||||||
masterPassword = mkOption {
|
masterPassword = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
description = "Password of the MySQL replication user";
|
description = "Password of the MySQL replication user.";
|
||||||
};
|
};
|
||||||
|
|
||||||
masterPort = mkOption {
|
masterPort = mkOption {
|
||||||
type = types.int;
|
type = types.int;
|
||||||
default = 3306;
|
default = 3306;
|
||||||
description = "Port number on which the MySQL master server runs";
|
description = "Port number on which the MySQL master server runs.";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -317,29 +321,33 @@ in
|
|||||||
binlog-ignore-db = [ "information_schema" "performance_schema" "mysql" ];
|
binlog-ignore-db = [ "information_schema" "performance_schema" "mysql" ];
|
||||||
})
|
})
|
||||||
(mkIf (!isMariaDB) {
|
(mkIf (!isMariaDB) {
|
||||||
plugin-load-add = optional (cfg.ensureUsers != []) "auth_socket.so";
|
plugin-load-add = "auth_socket.so";
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
users.users.mysql = {
|
users.users = optionalAttrs (cfg.user == "mysql") {
|
||||||
|
mysql = {
|
||||||
description = "MySQL server user";
|
description = "MySQL server user";
|
||||||
group = "mysql";
|
group = cfg.group;
|
||||||
uid = config.ids.uids.mysql;
|
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;
|
environment.etc."my.cnf".source = cfg.configFile;
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
"d '${cfg.dataDir}' 0700 ${cfg.user} mysql - -"
|
"d '${cfg.dataDir}' 0700 '${cfg.user}' '${cfg.group}' - -"
|
||||||
"z '${cfg.dataDir}' 0700 ${cfg.user} mysql - -"
|
"z '${cfg.dataDir}' 0700 '${cfg.user}' '${cfg.group}' - -"
|
||||||
];
|
];
|
||||||
|
|
||||||
systemd.services.mysql = let
|
systemd.services.mysql = let
|
||||||
hasNotify = (cfg.package == pkgs.mariadb);
|
hasNotify = isMariaDB;
|
||||||
in {
|
in {
|
||||||
description = "MySQL Server";
|
description = "MySQL Server";
|
||||||
|
|
||||||
@ -357,27 +365,20 @@ in
|
|||||||
|
|
||||||
preStart = if isMariaDB then ''
|
preStart = if isMariaDB then ''
|
||||||
if ! test -e ${cfg.dataDir}/mysql; then
|
if ! test -e ${cfg.dataDir}/mysql; then
|
||||||
${mysql}/bin/mysql_install_db --defaults-file=/etc/my.cnf ${mysqldOptions}
|
${cfg.package}/bin/mysql_install_db --defaults-file=/etc/my.cnf ${mysqldOptions}
|
||||||
touch ${cfg.dataDir}/mysql_init
|
touch ${cfg.dataDir}/mysql_init
|
||||||
fi
|
fi
|
||||||
'' else ''
|
'' else ''
|
||||||
if ! test -e ${cfg.dataDir}/mysql; then
|
if ! test -e ${cfg.dataDir}/mysql; then
|
||||||
${mysql}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} --initialize-insecure
|
${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} --initialize-insecure
|
||||||
touch ${cfg.dataDir}/mysql_init
|
touch ${cfg.dataDir}/mysql_init
|
||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
|
|
||||||
serviceConfig = {
|
postStart = let
|
||||||
Type = if hasNotify then "notify" else "simple";
|
# The super user account to use on *first* run of MySQL server
|
||||||
Restart = "on-abort";
|
superUser = if isMariaDB then cfg.user else "root";
|
||||||
RestartSec = "5s";
|
in ''
|
||||||
# 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) ''
|
${optionalString (!hasNotify) ''
|
||||||
# Wait until the MySQL server is available for use
|
# Wait until the MySQL server is available for use
|
||||||
count=0
|
count=0
|
||||||
@ -397,6 +398,12 @@ in
|
|||||||
|
|
||||||
if [ -f ${cfg.dataDir}/mysql_init ]
|
if [ -f ${cfg.dataDir}/mysql_init ]
|
||||||
then
|
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: ''
|
${concatMapStrings (database: ''
|
||||||
# Create initial databases
|
# Create initial databases
|
||||||
if ! test -e "${cfg.dataDir}/${database.name}"; then
|
if ! test -e "${cfg.dataDir}/${database.name}"; then
|
||||||
@ -416,7 +423,7 @@ in
|
|||||||
cat ${database.schema}/mysql-databases/*.sql
|
cat ${database.schema}/mysql-databases/*.sql
|
||||||
fi
|
fi
|
||||||
''}
|
''}
|
||||||
) | ${mysql}/bin/mysql -u root -N
|
) | ${cfg.package}/bin/mysql -u ${superUser} -N
|
||||||
fi
|
fi
|
||||||
'') cfg.initialDatabases}
|
'') cfg.initialDatabases}
|
||||||
|
|
||||||
@ -428,7 +435,7 @@ in
|
|||||||
echo "CREATE USER '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' IDENTIFIED WITH mysql_native_password;"
|
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 "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}';"
|
echo "GRANT REPLICATION SLAVE ON *.* TO '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}';"
|
||||||
) | ${mysql}/bin/mysql -u root -N
|
) | ${cfg.package}/bin/mysql -u ${superUser} -N
|
||||||
''}
|
''}
|
||||||
|
|
||||||
${optionalString (cfg.replication.role == "slave")
|
${optionalString (cfg.replication.role == "slave")
|
||||||
@ -438,7 +445,7 @@ in
|
|||||||
( echo "stop slave;"
|
( echo "stop slave;"
|
||||||
echo "change master to master_host='${cfg.replication.masterHost}', master_user='${cfg.replication.masterUser}', master_password='${cfg.replication.masterPassword}';"
|
echo "change master to master_host='${cfg.replication.masterHost}', master_user='${cfg.replication.masterUser}', master_password='${cfg.replication.masterPassword}';"
|
||||||
echo "start slave;"
|
echo "start slave;"
|
||||||
) | ${mysql}/bin/mysql -u root -N
|
) | ${cfg.package}/bin/mysql -u ${superUser} -N
|
||||||
''}
|
''}
|
||||||
|
|
||||||
${optionalString (cfg.initialScript != null)
|
${optionalString (cfg.initialScript != null)
|
||||||
@ -446,7 +453,7 @@ in
|
|||||||
# Execute initial script
|
# Execute initial script
|
||||||
# using toString to avoid copying the file to nix store if given as path instead of string,
|
# using toString to avoid copying the file to nix store if given as path instead of string,
|
||||||
# as it might contain credentials
|
# as it might contain credentials
|
||||||
cat ${toString cfg.initialScript} | ${mysql}/bin/mysql -u root -N
|
cat ${toString cfg.initialScript} | ${cfg.package}/bin/mysql -u ${superUser} -N
|
||||||
''}
|
''}
|
||||||
|
|
||||||
rm ${cfg.dataDir}/mysql_init
|
rm ${cfg.dataDir}/mysql_init
|
||||||
@ -457,7 +464,7 @@ in
|
|||||||
${concatMapStrings (database: ''
|
${concatMapStrings (database: ''
|
||||||
echo "CREATE DATABASE IF NOT EXISTS \`${database}\`;"
|
echo "CREATE DATABASE IF NOT EXISTS \`${database}\`;"
|
||||||
'') cfg.ensureDatabases}
|
'') cfg.ensureDatabases}
|
||||||
) | ${mysql}/bin/mysql -u root -N
|
) | ${cfg.package}/bin/mysql -N
|
||||||
''}
|
''}
|
||||||
|
|
||||||
${concatMapStrings (user:
|
${concatMapStrings (user:
|
||||||
@ -466,16 +473,19 @@ in
|
|||||||
${concatStringsSep "\n" (mapAttrsToList (database: permission: ''
|
${concatStringsSep "\n" (mapAttrsToList (database: permission: ''
|
||||||
echo "GRANT ${permission} ON ${database} TO '${user.name}'@'localhost';"
|
echo "GRANT ${permission} ON ${database} TO '${user.name}'@'localhost';"
|
||||||
'') user.ensurePermissions)}
|
'') user.ensurePermissions)}
|
||||||
) | ${mysql}/bin/mysql -u root -N
|
) | ${cfg.package}/bin/mysql -N
|
||||||
'') cfg.ensureUsers}
|
'') cfg.ensureUsers}
|
||||||
'';
|
'';
|
||||||
in
|
|
||||||
# ensureDatbases & ensureUsers depends on this script being run as root
|
serviceConfig = {
|
||||||
# when the user has secured their mysql install
|
Type = if hasNotify then "notify" else "simple";
|
||||||
"+${setupScript}";
|
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 and group
|
||||||
User = cfg.user;
|
User = cfg.user;
|
||||||
Group = "mysql";
|
Group = cfg.group;
|
||||||
# Runtime directory and mode
|
# Runtime directory and mode
|
||||||
RuntimeDirectory = "mysqld";
|
RuntimeDirectory = "mysqld";
|
||||||
RuntimeDirectoryMode = "0755";
|
RuntimeDirectoryMode = "0755";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user