From e0590da81322f9b8d496fcc6663d97c921690c2a Mon Sep 17 00:00:00 2001 From: Aaron Andersen Date: Sun, 30 Jun 2019 21:58:27 -0400 Subject: [PATCH 1/3] nixos/mysql: turn ExecStartPost into a shell script and simplify code --- nixos/modules/services/databases/mysql.nix | 179 ++++++++++----------- 1 file changed, 89 insertions(+), 90 deletions(-) diff --git a/nixos/modules/services/databases/mysql.nix b/nixos/modules/services/databases/mysql.nix index 02f406833131..c36aa96adf70 100644 --- a/nixos/modules/services/databases/mysql.nix +++ b/nixos/modules/services/databases/mysql.nix @@ -317,104 +317,103 @@ in RuntimeDirectoryMode = "0755"; # 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.writeShellScript "mysql-setup" '' + ${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 - postStart = - let - cmdWatchForMysqlSocket = '' - # 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 + ''} - echo "MySQL daemon not yet started. Waiting for 1 second..." - count=$((count++)) - sleep 1 - done - ''; - cmdInitialDatabases = concatMapStrings (database: '' - # Create initial databases - if ! test -e "${cfg.dataDir}/${database.name}"; then - echo "Creating initial database: ${database.name}" - ( echo 'create database `${database.name}`;' + if [ -f /tmp/mysql_init ] + 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}`;' + ${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 - ''} + # 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 "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 + ''} + + ${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;" + ) | ${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} | ${mysql}/bin/mysql -u root -N + ''} + + rm /tmp/mysql_init + fi + + ${optionalString (cfg.ensureDatabases != []) '' + ( + ${concatMapStrings (database: '' + echo "CREATE DATABASE IF NOT EXISTS \`${database}\`;" + '') cfg.ensureDatabases} ) | ${mysql}/bin/mysql -u root -N - fi - '') cfg.initialDatabases; - in + ''} - lib.optionalString (!hasNotify) cmdWatchForMysqlSocket + '' - if [ -f /tmp/mysql_init ] - then - ${cmdInitialDatabases} - ${optionalString (cfg.replication.role == "master") + ${concatMapStrings (user: '' - # 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}';" + ( 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 - ''} - - ${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;" - ) | ${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} | ${mysql}/bin/mysql -u root -N - ''} - - rm /tmp/mysql_init - fi - - ${optionalString (cfg.ensureDatabases != []) '' - ( - ${concatMapStrings (database: '' - echo "CREATE DATABASE IF NOT EXISTS \`${database}\`;" - '') cfg.ensureDatabases} - ) | ${mysql}/bin/mysql -u root -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} - - ''; # */ + '') cfg.ensureUsers} + ''; + in + setupScript; + }; }; }; From d0a147e841c84b83fd3f7ceed68c0c1a660b81e9 Mon Sep 17 00:00:00 2001 From: Aaron Andersen Date: Sun, 30 Jun 2019 21:59:47 -0400 Subject: [PATCH 2/3] nixos/mysql: run ExecStartPost as root (again) to preserve compatibility with installs that have been secured --- nixos/modules/services/databases/mysql.nix | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nixos/modules/services/databases/mysql.nix b/nixos/modules/services/databases/mysql.nix index c36aa96adf70..177e742d65e6 100644 --- a/nixos/modules/services/databases/mysql.nix +++ b/nixos/modules/services/databases/mysql.nix @@ -412,7 +412,9 @@ in '') cfg.ensureUsers} ''; in - setupScript; + # ensureDatbases & ensureUsers depends on this script being run as root + # when the user has secured their mysql install + "+${setupScript}"; }; }; From d9193f9edab2383c7f9285baf6b11007938b0d64 Mon Sep 17 00:00:00 2001 From: Aaron Andersen Date: Sun, 30 Jun 2019 22:01:02 -0400 Subject: [PATCH 3/3] nixos/mysql: secure access to database in one of the nixos tests --- nixos/tests/mysql.nix | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nixos/tests/mysql.nix b/nixos/tests/mysql.nix index cfe10bc41b0c..f712357b9ff0 100644 --- a/nixos/tests/mysql.nix +++ b/nixos/tests/mysql.nix @@ -28,6 +28,12 @@ import ./make-test.nix ({ pkgs, ...} : { { users.users.testuser = { }; services.mysql.enable = true; + services.mysql.initialScript = pkgs.writeText "mariadb-init.sql" '' + echo "ALTER USER root@localhost IDENTIFIED WITH unix_socket;" + echo "DELETE FROM mysql.user WHERE password = ''' AND plugin = ''';" + echo "DELETE FROM mysql.user WHERE user = ''';" + echo "FLUSH PRIVILEGES;" + ''; services.mysql.ensureDatabases = [ "testdb" ]; services.mysql.ensureUsers = [{ name = "testuser";