mirror of
https://github.com/NixOS/nixpkgs.git
synced 2026-07-06 17:13:24 -05:00
services.mysql.initalScript runs with the root@localhost mysql user.
This seems consistent with the other invocations that run in the first
startup of mysql.
After the security fix, the mysql command executing the initial script
was broken as it tried to connect to root@localhost while being the mysql
system user.
We could instead use the mysql@localhost mysql user, but this would be
inconsistent with the other invocations.
Co-Authored-By: osnyx <os@flyingcircus.io>
(cherry picked from commit 6f5d20fe0c)
782 lines
29 KiB
Nix
782 lines
29 KiB
Nix
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
let
|
|
|
|
cfg = config.services.mysql;
|
|
|
|
isMariaDB = lib.getName cfg.package == lib.getName pkgs.mariadb;
|
|
|
|
mysqldOptions = "--user=${cfg.user} --datadir=${cfg.dataDir} --basedir=${cfg.package}";
|
|
|
|
format = pkgs.formats.ini { listsAsDuplicateKeys = true; };
|
|
configFile = format.generate "my.cnf" cfg.settings;
|
|
|
|
generateClusterAddressExpr = ''
|
|
if (config.services.mysql.galeraCluster.nodeAddresses == [ ]) then
|
|
""
|
|
else
|
|
"gcomm://''${builtins.concatStringsSep \",\" config.services.mysql.galeraCluster.nodeAddresses}"
|
|
+ lib.optionalString (config.services.mysql.galeraCluster.clusterPassword != "")
|
|
"?gmcast.seg=1:''${config.services.mysql.galeraCluster.clusterPassword}"
|
|
'';
|
|
generateClusterAddress =
|
|
if (cfg.galeraCluster.nodeAddresses == [ ]) then
|
|
""
|
|
else
|
|
"gcomm://${builtins.concatStringsSep "," cfg.galeraCluster.nodeAddresses}"
|
|
+ lib.optionalString (
|
|
cfg.galeraCluster.clusterPassword != ""
|
|
) "?gmcast.seg=1:${cfg.galeraCluster.clusterPassword}";
|
|
in
|
|
|
|
{
|
|
imports = [
|
|
(lib.mkRemovedOptionModule [
|
|
"services"
|
|
"mysql"
|
|
"pidDir"
|
|
] "Don't wait for pidfiles, describe dependencies through systemd.")
|
|
(lib.mkRemovedOptionModule [
|
|
"services"
|
|
"mysql"
|
|
"rootPassword"
|
|
] "Use socket authentication or set the password outside of the nix store.")
|
|
(lib.mkRemovedOptionModule [
|
|
"services"
|
|
"mysql"
|
|
"extraOptions"
|
|
] "Use services.mysql.settings.mysqld instead.")
|
|
(lib.mkRemovedOptionModule [
|
|
"services"
|
|
"mysql"
|
|
"bind"
|
|
] "Use services.mysql.settings.mysqld.bind-address instead.")
|
|
(lib.mkRemovedOptionModule [
|
|
"services"
|
|
"mysql"
|
|
"port"
|
|
] "Use services.mysql.settings.mysqld.port instead.")
|
|
];
|
|
|
|
###### interface
|
|
|
|
options = {
|
|
|
|
services.mysql = {
|
|
|
|
enable = lib.mkEnableOption "MySQL server";
|
|
|
|
package = lib.mkOption {
|
|
type = lib.types.package;
|
|
example = lib.literalExpression "pkgs.mariadb";
|
|
description = ''
|
|
Which MySQL derivation to use. MariaDB packages are supported too.
|
|
'';
|
|
};
|
|
|
|
user = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "mysql";
|
|
description = ''
|
|
User account under which MySQL runs.
|
|
|
|
::: {.note}
|
|
If left as the default value this user will automatically be created
|
|
on system activation, otherwise you are responsible for
|
|
ensuring the user exists before the MySQL service starts.
|
|
:::
|
|
'';
|
|
};
|
|
|
|
group = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "mysql";
|
|
description = ''
|
|
Group account under which MySQL runs.
|
|
|
|
::: {.note}
|
|
If left as the default value this group will automatically be created
|
|
on system activation, otherwise you are responsible for
|
|
ensuring the user exists before the MySQL service starts.
|
|
:::
|
|
'';
|
|
};
|
|
|
|
dataDir = lib.mkOption {
|
|
type = lib.types.path;
|
|
default = (
|
|
if lib.versionAtLeast config.system.stateVersion "17.09" then "/var/lib/mysql" else "/var/mysql"
|
|
);
|
|
example = "/var/lib/mysql";
|
|
description = ''
|
|
The data directory for MySQL.
|
|
|
|
::: {.note}
|
|
If left as the default value of `/var/lib/mysql` this directory will automatically be created before the MySQL
|
|
server starts, otherwise you are responsible for ensuring the directory exists with appropriate ownership and permissions.
|
|
:::
|
|
'';
|
|
};
|
|
|
|
configFile = lib.mkOption {
|
|
type = lib.types.path;
|
|
default = configFile;
|
|
defaultText = ''
|
|
A configuration file automatically generated by NixOS.
|
|
'';
|
|
description = ''
|
|
Override the configuration file used by MySQL. By default,
|
|
NixOS generates one automatically from {option}`services.mysql.settings`.
|
|
'';
|
|
example = lib.literalExpression ''
|
|
pkgs.writeText "my.cnf" '''
|
|
[mysqld]
|
|
datadir = /var/lib/mysql
|
|
bind-address = 127.0.0.1
|
|
port = 3336
|
|
|
|
!includedir /etc/mysql/conf.d/
|
|
''';
|
|
'';
|
|
};
|
|
|
|
settings = lib.mkOption {
|
|
type = format.type;
|
|
default = { };
|
|
description = ''
|
|
MySQL configuration. Refer to
|
|
<https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html>,
|
|
<https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html>,
|
|
and <https://mariadb.com/kb/en/server-system-variables/>
|
|
for details on supported values.
|
|
|
|
::: {.note}
|
|
MySQL configuration options such as `--quick` should be treated as
|
|
boolean options and provided values such as `true`, `false`,
|
|
`1`, or `0`. See the provided example below.
|
|
:::
|
|
'';
|
|
example = lib.literalExpression ''
|
|
{
|
|
mysqld = {
|
|
key_buffer_size = "6G";
|
|
table_cache = 1600;
|
|
log-error = "/var/log/mysql_err.log";
|
|
plugin-load-add = [ "server_audit" "ed25519=auth_ed25519" ];
|
|
};
|
|
mysqldump = {
|
|
quick = true;
|
|
max_allowed_packet = "16M";
|
|
};
|
|
}
|
|
'';
|
|
};
|
|
|
|
initialDatabases = lib.mkOption {
|
|
type = lib.types.listOf (
|
|
lib.types.submodule {
|
|
options = {
|
|
name = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = ''
|
|
The name of the database to create.
|
|
'';
|
|
};
|
|
schema = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
description = ''
|
|
The initial schema of the database; if null (the default),
|
|
an empty database is created.
|
|
'';
|
|
};
|
|
};
|
|
}
|
|
);
|
|
default = [ ];
|
|
description = ''
|
|
List of database names and their initial schemas that should be used to create databases on the first startup
|
|
of MySQL. The schema attribute is optional: If not specified, an empty database is created.
|
|
'';
|
|
example = lib.literalExpression ''
|
|
[
|
|
{ name = "foodatabase"; schema = ./foodatabase.sql; }
|
|
{ name = "bardatabase"; }
|
|
]
|
|
'';
|
|
};
|
|
|
|
initialScript = lib.mkOption {
|
|
type = lib.types.nullOr lib.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.";
|
|
};
|
|
|
|
ensureDatabases = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ ];
|
|
description = ''
|
|
Ensures that the specified databases exist.
|
|
This option will never delete existing databases, especially not when the value of this
|
|
option is changed. This means that databases created once through this option or
|
|
otherwise have to be removed manually.
|
|
'';
|
|
example = [
|
|
"nextcloud"
|
|
"matomo"
|
|
];
|
|
};
|
|
|
|
ensureUsers = lib.mkOption {
|
|
type = lib.types.listOf (
|
|
lib.types.submodule {
|
|
options = {
|
|
name = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = ''
|
|
Name of the user to ensure.
|
|
'';
|
|
};
|
|
ensurePermissions = lib.mkOption {
|
|
type = lib.types.attrsOf lib.types.str;
|
|
default = { };
|
|
description = ''
|
|
Permissions to ensure for the user, specified as attribute set.
|
|
The attribute names specify the database and tables to grant the permissions for,
|
|
separated by a dot. You may use wildcards here.
|
|
The attribute values specfiy the permissions to grant.
|
|
You may specify one or multiple comma-separated SQL privileges here.
|
|
|
|
For more information on how to specify the target
|
|
and on which privileges exist, see the
|
|
[GRANT syntax](https://mariadb.com/kb/en/library/grant/).
|
|
The attributes are used as `GRANT ''${attrName} ON ''${attrValue}`.
|
|
'';
|
|
example = lib.literalExpression ''
|
|
{
|
|
"database.*" = "ALL PRIVILEGES";
|
|
"*.*" = "SELECT, LOCK TABLES";
|
|
}
|
|
'';
|
|
};
|
|
};
|
|
}
|
|
);
|
|
default = [ ];
|
|
description = ''
|
|
Ensures that the specified users exist and have at least the ensured permissions.
|
|
The MySQL users will be identified using Unix socket authentication. This authenticates the Unix user with the
|
|
same name only, and that without the need for a password.
|
|
This option will never delete existing users or remove permissions, especially not when the value of this
|
|
option is changed. This means that users created and permissions assigned once through this option or
|
|
otherwise have to be removed manually.
|
|
'';
|
|
example = lib.literalExpression ''
|
|
[
|
|
{
|
|
name = "nextcloud";
|
|
ensurePermissions = {
|
|
"nextcloud.*" = "ALL PRIVILEGES";
|
|
};
|
|
}
|
|
{
|
|
name = "backup";
|
|
ensurePermissions = {
|
|
"*.*" = "SELECT, LOCK TABLES";
|
|
};
|
|
}
|
|
]
|
|
'';
|
|
};
|
|
|
|
secureSuperUserByDefault = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = ''
|
|
Whether to automatically secure the root@localhost user with auth_socket authentication.
|
|
|
|
::: {.note}
|
|
When enabled (default), the module will ensure root@localhost uses socket authentication,
|
|
preventing any local user from connecting as root without proper credentials.
|
|
:::
|
|
'';
|
|
};
|
|
|
|
replication = {
|
|
role = lib.mkOption {
|
|
type = lib.types.enum [
|
|
"master"
|
|
"slave"
|
|
"none"
|
|
];
|
|
default = "none";
|
|
description = "Role of the MySQL server instance.";
|
|
};
|
|
|
|
serverId = lib.mkOption {
|
|
type = lib.types.int;
|
|
default = 1;
|
|
description = "Id of the MySQL server instance. This number must be unique for each instance.";
|
|
};
|
|
|
|
masterHost = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "Hostname of the MySQL master server.";
|
|
};
|
|
|
|
slaveHost = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "Hostname of the MySQL slave server.";
|
|
};
|
|
|
|
masterUser = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "Username of the MySQL replication user.";
|
|
};
|
|
|
|
masterPassword = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "Password of the MySQL replication user.";
|
|
};
|
|
|
|
masterPort = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 3306;
|
|
description = "Port number on which the MySQL master server runs.";
|
|
};
|
|
};
|
|
|
|
galeraCluster = {
|
|
enable = lib.mkEnableOption "MariaDB Galera Cluster";
|
|
|
|
package = lib.mkOption {
|
|
type = lib.types.package;
|
|
description = "The MariaDB Galera package that provides the shared library 'libgalera_smm.so' required for cluster functionality.";
|
|
default = lib.literalExpression "pkgs.mariadb-galera";
|
|
};
|
|
|
|
name = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "The logical name of the Galera cluster. All nodes in the same cluster must use the same name.";
|
|
default = "galera";
|
|
};
|
|
|
|
sstMethod = lib.mkOption {
|
|
type = lib.types.enum [
|
|
"rsync"
|
|
"mariabackup"
|
|
];
|
|
description = "Method for the initial state transfer (wsrep_sst_method) when a node joins the cluster. Be aware that rsync needs SSH keys to be generated and authorized on all nodes!";
|
|
default = "rsync";
|
|
example = "mariabackup";
|
|
};
|
|
|
|
localName = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "The unique name that identifies this particular node within the cluster. Each node must have a different name.";
|
|
example = "node1";
|
|
};
|
|
|
|
localAddress = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "IP address or hostname of this node that will be used for cluster communication. Must be reachable by all other nodes.";
|
|
example = "1.2.3.4";
|
|
default = cfg.galeraCluster.localName;
|
|
defaultText = lib.literalExpression "config.services.mysql.galeraCluster.localName";
|
|
};
|
|
|
|
nodeAddresses = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
description = "IP addresses or hostnames of all nodes in the cluster, including this node. This is used to construct the default clusterAddress connection string.";
|
|
example = lib.literalExpression ''["10.0.0.10" "10.0.0.20" "10.0.0.30"]'';
|
|
default = [ ];
|
|
};
|
|
|
|
clusterPassword = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "Optional password for securing cluster communications. If provided, it will be used in the clusterAddress for authentication between nodes.";
|
|
example = "SomePassword";
|
|
default = "";
|
|
};
|
|
|
|
clusterAddress = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "Full Galera cluster connection string. If nodeAddresses is set, this will be auto-generated, but you can override it with a custom value. Format is typically 'gcomm://node1,node2,node3' with optional parameters.";
|
|
example = "gcomm://10.0.0.10,10.0.0.20,10.0.0.30?gmcast.seg=1:SomePassword";
|
|
default = ""; # will be evaluate by generateClusterAddress
|
|
defaultText = lib.literalExpression generateClusterAddressExpr;
|
|
};
|
|
|
|
};
|
|
};
|
|
|
|
};
|
|
|
|
###### implementation
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
assertions = [
|
|
{
|
|
assertion = !cfg.galeraCluster.enable || isMariaDB;
|
|
message = "'services.mysql.galeraCluster.enable' expect services.mysql.package to be an mariadb variant";
|
|
}
|
|
{
|
|
assertion = !isMariaDB || cfg.secureSuperUserByDefault == true;
|
|
message = "'services.mysql.secureSuperUserByDefault' has no effect on MariaDB (which is already secure by default)";
|
|
}
|
|
]
|
|
# galeraCluster options checks
|
|
++ lib.optionals cfg.galeraCluster.enable [
|
|
{
|
|
assertion =
|
|
cfg.galeraCluster.localAddress != ""
|
|
&& (cfg.galeraCluster.nodeAddresses != [ ] || cfg.galeraCluster.clusterAddress != "");
|
|
message = "mariadb galera cluster is enabled but the localAddress and (nodeAddresses or clusterAddress) are not set";
|
|
}
|
|
{
|
|
assertion = cfg.galeraCluster.clusterPassword == "" || cfg.galeraCluster.clusterAddress == "";
|
|
message = "mariadb galera clusterPassword is set but overwritten by clusterAddress";
|
|
}
|
|
{
|
|
assertion = cfg.galeraCluster.nodeAddresses != [ ] || cfg.galeraCluster.clusterAddress != "";
|
|
message = "When services.mysql.galeraCluster.clusterAddress is set, setting services.mysql.galeraCluster.nodeAddresses is redundant and will be overwritten by clusterAddress. Choose one approach.";
|
|
}
|
|
];
|
|
|
|
services.mysql.settings.mysqld = lib.mkMerge [
|
|
{
|
|
datadir = cfg.dataDir;
|
|
port = lib.mkDefault 3306;
|
|
}
|
|
(lib.mkIf (cfg.replication.role == "master" || cfg.replication.role == "slave") {
|
|
log-bin = "mysql-bin-${toString cfg.replication.serverId}";
|
|
log-bin-index = "mysql-bin-${toString cfg.replication.serverId}.index";
|
|
relay-log = "mysql-relay-bin";
|
|
server-id = cfg.replication.serverId;
|
|
binlog-ignore-db = [
|
|
"information_schema"
|
|
"performance_schema"
|
|
"mysql"
|
|
];
|
|
})
|
|
(lib.mkIf (!isMariaDB) {
|
|
plugin-load-add = [ "auth_socket.so" ];
|
|
})
|
|
(lib.mkIf cfg.galeraCluster.enable {
|
|
# Ensure Only InnoDB is used as galera clusters can only work with them
|
|
enforce_storage_engine = "InnoDB";
|
|
default_storage_engine = "InnoDB";
|
|
|
|
# galera only support this binlog format
|
|
binlog-format = "ROW";
|
|
|
|
bind_address = lib.mkDefault "0.0.0.0";
|
|
})
|
|
];
|
|
|
|
services.mysql.settings.galera = lib.optionalAttrs cfg.galeraCluster.enable {
|
|
wsrep_on = "ON";
|
|
wsrep_debug = lib.mkDefault "NONE";
|
|
wsrep_retry_autocommit = lib.mkDefault "3";
|
|
wsrep_provider = "${cfg.galeraCluster.package}/lib/galera/libgalera_smm.so";
|
|
|
|
wsrep_cluster_name = cfg.galeraCluster.name;
|
|
wsrep_cluster_address =
|
|
if (cfg.galeraCluster.clusterAddress != "") then
|
|
cfg.galeraCluster.clusterAddress
|
|
else
|
|
generateClusterAddress;
|
|
|
|
wsrep_node_address = cfg.galeraCluster.localAddress;
|
|
wsrep_node_name = "${cfg.galeraCluster.localName}";
|
|
|
|
# SST method using rsync
|
|
wsrep_sst_method = lib.mkDefault cfg.galeraCluster.sstMethod;
|
|
wsrep_sst_auth = lib.mkDefault "check_repl:check_pass";
|
|
|
|
binlog_format = "ROW";
|
|
innodb_autoinc_lock_mode = 2;
|
|
};
|
|
|
|
users.users = lib.optionalAttrs (cfg.user == "mysql") {
|
|
mysql = {
|
|
description = "MySQL server user";
|
|
group = cfg.group;
|
|
uid = config.ids.uids.mysql;
|
|
};
|
|
};
|
|
|
|
users.groups = lib.optionalAttrs (cfg.group == "mysql") {
|
|
mysql.gid = config.ids.gids.mysql;
|
|
};
|
|
|
|
environment.systemPackages = [ cfg.package ];
|
|
|
|
environment.etc."my.cnf".source = cfg.configFile;
|
|
|
|
# The mysql_install_db binary will try to adjust the permissions, but fail to do so with a permission
|
|
# denied error in some circumstances. Setting the permissions manually with tmpfiles is a workaround.
|
|
systemd.tmpfiles.rules = [
|
|
"d ${cfg.dataDir} 0755 ${cfg.user} ${cfg.group} - -"
|
|
];
|
|
|
|
systemd.services.mysql = {
|
|
description = "MySQL Server";
|
|
|
|
after = [ "network.target" ];
|
|
wantedBy = [ "multi-user.target" ];
|
|
restartTriggers = [ cfg.configFile ];
|
|
|
|
unitConfig.RequiresMountsFor = cfg.dataDir;
|
|
|
|
path = [
|
|
# Needed for the mysql_install_db command in the preStart script
|
|
# which calls the hostname command.
|
|
pkgs.hostname-debian
|
|
]
|
|
# tools 'wsrep_sst_rsync' needs
|
|
++ lib.optionals cfg.galeraCluster.enable [
|
|
cfg.package
|
|
pkgs.bash
|
|
pkgs.gawk
|
|
pkgs.gnutar
|
|
pkgs.gzip
|
|
pkgs.inetutils
|
|
pkgs.iproute2
|
|
pkgs.netcat
|
|
pkgs.procps
|
|
pkgs.pv
|
|
pkgs.rsync
|
|
pkgs.socat
|
|
pkgs.stunnel
|
|
pkgs.which
|
|
];
|
|
|
|
preStart =
|
|
if isMariaDB then
|
|
''
|
|
if ! test -e ${cfg.dataDir}/mysql; then
|
|
${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
|
|
${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} --initialize-insecure
|
|
touch ${cfg.dataDir}/mysql_init
|
|
fi
|
|
'';
|
|
|
|
script = ''
|
|
# https://mariadb.com/kb/en/getting-started-with-mariadb-galera-cluster/#systemd-and-galera-recovery
|
|
if test -n "''${_WSREP_START_POSITION}"; then
|
|
if test -e "${cfg.package}/bin/galera_recovery"; then
|
|
VAR=$(cd ${cfg.package}/bin/..; ${cfg.package}/bin/galera_recovery); [[ $? -eq 0 ]] && export _WSREP_START_POSITION=$VAR || exit 1
|
|
fi
|
|
fi
|
|
|
|
# The last two environment variables are used for starting Galera clusters
|
|
exec ${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} $_WSREP_NEW_CLUSTER $_WSREP_START_POSITION
|
|
'';
|
|
|
|
postStart =
|
|
let
|
|
# The super user account to use on *first* run of MySQL server
|
|
superUser = if isMariaDB then cfg.user else "root";
|
|
isStateVersion2611Plus = lib.versionAtLeast config.system.stateVersion "26.11";
|
|
in
|
|
''
|
|
${lib.optionalString isMariaDB ''
|
|
# If MariaDB is used in an Galera cluster, we have to check if the sync is done,
|
|
# or it will fail to init the database while joining, so we get in an broken non recoverable state
|
|
# so we wait until we have an synced state
|
|
if ${cfg.package}/bin/mysql -u ${superUser} -N -e "SHOW VARIABLES LIKE 'wsrep_on'" 2>/dev/null | ${lib.getExe' pkgs.gnugrep "grep"} -q 'ON'; then
|
|
echo "Galera cluster detected, waiting for node to be synced..."
|
|
while true; do
|
|
STATE=$(${cfg.package}/bin/mysql -u ${superUser} -N -e "SHOW STATUS LIKE 'wsrep_local_state_comment'" | ${lib.getExe' pkgs.gawk "awk"} '{print $2}')
|
|
if [ "$STATE" = "Synced" ]; then
|
|
echo "Node is synced"
|
|
break
|
|
else
|
|
echo "Current state: $STATE - Waiting for 1 second..."
|
|
sleep 1
|
|
fi
|
|
done
|
|
fi
|
|
''}
|
|
|
|
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
|
|
|
|
${lib.concatMapStrings (database: ''
|
|
# Create initial databases
|
|
if ! test -e "${cfg.dataDir}/${database.name}"; then
|
|
echo "Creating initial database: ${database.name}"
|
|
( echo 'CREATE DATABASE IF NOT EXISTS `${database.name}`;'
|
|
|
|
${lib.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}
|
|
|
|
${lib.optionalString (cfg.replication.role == "master") ''
|
|
# Set up the replication master
|
|
|
|
( 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
|
|
''}
|
|
|
|
${lib.optionalString (cfg.replication.role == "slave") ''
|
|
# Set up the replication slave
|
|
|
|
( 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
|
|
''}
|
|
|
|
${lib.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
|
|
''}
|
|
|
|
# Secure root@localhost for MySQL/Percona on first initialization
|
|
${lib.optionalString (cfg.secureSuperUserByDefault && !isMariaDB) ''
|
|
echo "ALTER USER root@localhost IDENTIFIED WITH auth_socket;" | ${cfg.package}/bin/mysql -u ${superUser} -N
|
|
''}
|
|
|
|
rm ${cfg.dataDir}/mysql_init
|
|
fi
|
|
|
|
${lib.optionalString (cfg.secureSuperUserByDefault && !isMariaDB) ''
|
|
# We try to detect if we are in the default insecure auth mode for MySQL (all users can connect with password)
|
|
# If the configuration has been moved to the socket-peer credential authentication we do nothing
|
|
# If we are not able to connect it also means the default setup has been adjusted, so we also skip and do not do any changes
|
|
if plugin_info=$(${cfg.package}/bin/mysql -u ${superUser} --skip-column-names 2>/dev/null -e "SELECT plugin FROM mysql.user WHERE user = 'root' AND host = 'localhost';"); then
|
|
case "$plugin_info" in
|
|
*auth_socket*) ;;
|
|
*)
|
|
${lib.optionalString isStateVersion2611Plus ''
|
|
# Attempt to auto-fix to prevent local authentication without a password
|
|
echo "Securing root@localhost with auth_socket to local connection without password, see https://github.com/NixOS/nixpkgs/security/advisories/GHSA-6qxx-6rg8-c4p8" >&2
|
|
echo "ALTER USER root@localhost IDENTIFIED WITH auth_socket;" | ${cfg.package}/bin/mysql -u ${superUser} -N
|
|
''}
|
|
${lib.optionalString (!isStateVersion2611Plus) ''
|
|
echo "Security warning: root@localhost seems to have open authentication, consider adjusting your configuration. See https://github.com/NixOS/nixpkgs/security/advisories/GHSA-6qxx-6rg8-c4p8" >&2
|
|
''}
|
|
;;
|
|
esac
|
|
fi
|
|
''}
|
|
|
|
${lib.optionalString (cfg.ensureDatabases != [ ]) ''
|
|
(
|
|
${lib.concatMapStrings (database: ''
|
|
echo "CREATE DATABASE IF NOT EXISTS \`${database}\`;"
|
|
'') cfg.ensureDatabases}
|
|
) | ${cfg.package}/bin/mysql -N
|
|
''}
|
|
|
|
${lib.concatMapStrings (user: ''
|
|
( echo "CREATE USER IF NOT EXISTS '${user.name}'@'localhost' IDENTIFIED WITH ${
|
|
if isMariaDB then "unix_socket" else "auth_socket"
|
|
};"
|
|
${lib.concatStringsSep "\n" (
|
|
lib.mapAttrsToList (database: permission: ''
|
|
echo "GRANT ${permission} ON ${database} TO '${user.name}'@'localhost';"
|
|
'') user.ensurePermissions
|
|
)}
|
|
) | ${cfg.package}/bin/mysql -N
|
|
'') cfg.ensureUsers}
|
|
'';
|
|
|
|
serviceConfig = lib.mkMerge [
|
|
{
|
|
Type = "notify";
|
|
Restart = "on-abnormal";
|
|
RestartSec = "5s";
|
|
|
|
# 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";
|
|
}
|
|
(lib.mkIf (cfg.dataDir == "/var/lib/mysql") {
|
|
StateDirectory = "mysql";
|
|
StateDirectoryMode = "0700";
|
|
})
|
|
];
|
|
};
|
|
|
|
# Open firewall ports for MySQL (and Galera)
|
|
networking.firewall.allowedTCPPorts = lib.optionals cfg.galeraCluster.enable [
|
|
3306 # MySQL
|
|
4567 # Galera Cluster
|
|
4568 # Galera IST
|
|
4444 # SST
|
|
];
|
|
networking.firewall.allowedUDPPorts = lib.optionals cfg.galeraCluster.enable [
|
|
4567 # Galera Cluster
|
|
];
|
|
};
|
|
|
|
meta.maintainers = [ lib.maintainers._6543 ];
|
|
}
|