diff --git a/mysql-test/main/mysql_secure_installation.result b/mysql-test/main/mysql_secure_installation.result new file mode 100644 index 0000000000000..fc2e241e7d856 --- /dev/null +++ b/mysql-test/main/mysql_secure_installation.result @@ -0,0 +1,24 @@ +# +# MDEV-10112: mysql_secure_installation should use DDL (GRANT, REVOKE, etc) +# instead of DML for Galera compatibility +# +# +# Setup dummy accounts and database to be secured +# +CREATE USER 'root'@'remote_host' IDENTIFIED BY 'pass'; +CREATE USER 'test_user'@'localhost' IDENTIFIED BY 'pass'; +CREATE DATABASE msi_test_db; +GRANT ALL PRIVILEGES ON msi_test_db.* TO 'test_user'@'localhost'; +GRANT ALL PRIVILEGES ON test.* TO 'test_user'@'localhost'; +# Verify: remote root account removed +SELECT user, host FROM mysql.global_priv WHERE user = 'root' AND host = 'remote_host'; +user host +# Verify: test database removed +SHOW DATABASES LIKE 'test'; +Database (test) +# Verify: test DB privileges removed from mysql.db +SELECT user, host, db FROM mysql.db WHERE db = 'test' ORDER BY user, host, db; +user host db +# Cleanup +DROP USER 'test_user'@'localhost'; +DROP DATABASE msi_test_db; diff --git a/mysql-test/main/mysql_secure_installation.test b/mysql-test/main/mysql_secure_installation.test new file mode 100644 index 0000000000000..8715f850ed102 --- /dev/null +++ b/mysql-test/main/mysql_secure_installation.test @@ -0,0 +1,66 @@ +--source include/not_windows.inc +--source include/not_embedded.inc +--source include/save_sys_tables.inc + +--echo # +--echo # MDEV-10112: mysql_secure_installation should use DDL (GRANT, REVOKE, etc) +--echo # instead of DML for Galera compatibility +--echo # + +--echo # +--echo # Setup dummy accounts and database to be secured +--echo # +CREATE USER 'root'@'remote_host' IDENTIFIED BY 'pass'; +CREATE USER 'test_user'@'localhost' IDENTIFIED BY 'pass'; + +# 'test' is a special database in MTR, ensuring it exists for the script +--disable_warnings +CREATE DATABASE IF NOT EXISTS test; +--enable_warnings + +# Create a custom database to ensure we don't interfere with other tests +CREATE DATABASE msi_test_db; +GRANT ALL PRIVILEGES ON msi_test_db.* TO 'test_user'@'localhost'; + +# Verify the script's REVOKE logic by granting on 'test' +GRANT ALL PRIVILEGES ON test.* TO 'test_user'@'localhost'; + +# Prepare input for the script +# 1. [Empty] - current root password +# 2. n - Enable unix_socket? +# 3. n - Set root password? +# 4. Y - Remove anonymous users? +# 5. Y - Disallow root login remotely? +# 6. Y - Remove test database? +# 7. Y - Reload privilege tables? +--write_file $MYSQLTEST_VARDIR/tmp/msi_input.txt + +n +n +Y +Y +Y +Y +EOF + +# Run the script +--exec $MYSQL_SECURE_INSTALLATION --basedir=$MYSQL_BINDIR -S $MASTER_MYSOCK < $MYSQLTEST_VARDIR/tmp/msi_input.txt > $MYSQLTEST_VARDIR/tmp/msi.log 2>&1 + +--remove_file $MYSQLTEST_VARDIR/tmp/msi_input.txt +--remove_file $MYSQLTEST_VARDIR/tmp/msi.log + +--echo # Verify: remote root account removed +SELECT user, host FROM mysql.global_priv WHERE user = 'root' AND host = 'remote_host'; + +--echo # Verify: test database removed +SHOW DATABASES LIKE 'test'; + +--echo # Verify: test DB privileges removed from mysql.db +SELECT user, host, db FROM mysql.db WHERE db = 'test' ORDER BY user, host, db; + +# Cleanup +# The script already dropped 'root'@'remote_host' and 'test' DB. +DROP USER 'test_user'@'localhost'; +DROP DATABASE msi_test_db; + +--source include/restore_sys_tables.inc diff --git a/mysql-test/mariadb-test-run.pl b/mysql-test/mariadb-test-run.pl index ee3412c9cd154..433ba983ecab8 100755 --- a/mysql-test/mariadb-test-run.pl +++ b/mysql-test/mariadb-test-run.pl @@ -2233,12 +2233,13 @@ sub environment_setup { $ENV{'MYSQL_PLUGIN'}= $exe_mysql_plugin; $ENV{'MYSQL_EMBEDDED'}= $exe_mysql_embedded; $ENV{'MARIADB_CONV'}= "$exe_mariadb_conv --character-sets-dir=$path_charsetsdir"; + $ENV{'MYSQL_INSTALL_DB_EXE'}= mtr_exe_maybe_exists("$bindir/sql/mariadb-install-db", + "$bindir/bin/mariadb-install-db", + "$bindir/scripts/mariadb-install-db"); if(IS_WINDOWS) { - $ENV{'MYSQL_INSTALL_DB_EXE'}= mtr_exe_exists("$bindir/sql$multiconfig/mariadb-install-db", - "$bindir/bin/mariadb-install-db"); $ENV{'MARIADB_UPGRADE_SERVICE_EXE'}= mtr_exe_exists("$bindir/sql$multiconfig/mariadb-upgrade-service", - "$bindir/bin/mariadb-upgrade-service"); + "$bindir/bin/mariadb-upgrade-service"); $ENV{'MARIADB_UPGRADE_EXE'}= mtr_exe_exists("$path_client_bindir/mariadb-upgrade"); } @@ -2312,6 +2313,19 @@ sub environment_setup { $ENV{'MYSQLHOTCOPY'}= $mysqlhotcopy; } + # ---------------------------------------------------- + # mysql_secure_installation + # ---------------------------------------------------- + my $mysql_secure_installation= + mtr_pl_maybe_exists("$bindir/scripts/mariadb-secure-installation") || + mtr_pl_maybe_exists("$bindir/scripts/mysql_secure_installation") || + mtr_pl_maybe_exists("$path_client_bindir/mariadb-secure-installation") || + mtr_pl_maybe_exists("$path_client_bindir/mysql_secure_installation"); + if ($mysql_secure_installation) + { + $ENV{'MYSQL_SECURE_INSTALLATION'}= $mysql_secure_installation; + } + # ---------------------------------------------------- # perror # ---------------------------------------------------- diff --git a/mysql-test/suite/galera/r/galera_secure_installation.result b/mysql-test/suite/galera/r/galera_secure_installation.result new file mode 100644 index 0000000000000..5a475646f4a8d --- /dev/null +++ b/mysql-test/suite/galera/r/galera_secure_installation.result @@ -0,0 +1,30 @@ +# +# MDEV-10112: mysql_secure_installation should use DDL (GRANT, REVOKE, etc) +# instead of DML for Galera compatibility +# +# On node_1 +# +# Setup remote root account to be removed +# +CREATE USER 'root'@'remote_host' IDENTIFIED BY 'pass'; +GRANT ALL ON *.* TO 'root'@'remote_host'; +# On node_2 +SELECT user, host FROM mysql.global_priv WHERE user = 'root' AND host = 'remote_host'; +user host +root remote_host +SHOW DATABASES LIKE 'test'; +Database (test) +test +# Verify on node_1: remote root is gone +SELECT user, host FROM mysql.global_priv WHERE user = 'root' AND host = 'remote_host'; +user host +# Verify on node_1: test database is gone +SHOW DATABASES LIKE 'test'; +Database (test) +# Now verify on node_2 (replication check) +# Verify on node_2: remote root is gone (replicated) +SELECT user, host FROM mysql.global_priv WHERE user = 'root' AND host = 'remote_host'; +user host +# Verify on node_2: test database is gone (replicated) +SHOW DATABASES LIKE 'test'; +Database (test) diff --git a/mysql-test/suite/galera/t/galera_secure_installation.test b/mysql-test/suite/galera/t/galera_secure_installation.test new file mode 100644 index 0000000000000..b06ed4680569c --- /dev/null +++ b/mysql-test/suite/galera/t/galera_secure_installation.test @@ -0,0 +1,66 @@ +--source include/galera_cluster.inc +--source include/have_innodb.inc + +--echo # +--echo # MDEV-10112: mysql_secure_installation should use DDL (GRANT, REVOKE, etc) +--echo # instead of DML for Galera compatibility +--echo # + +--connection node_1 +--echo # +--echo # Setup remote root account to be removed +--echo # +CREATE USER 'root'@'remote_host' IDENTIFIED BY 'pass'; +GRANT ALL ON *.* TO 'root'@'remote_host'; + +# 'test' is a special database in MTR, ensuring it exists for the script +--disable_warnings +CREATE DATABASE IF NOT EXISTS test; +--enable_warnings + +--connection node_2 +--let $wait_condition = SELECT COUNT(*) = 1 FROM mysql.global_priv WHERE user = 'root' AND host = 'remote_host' +--source include/wait_condition.inc +SELECT user, host FROM mysql.global_priv WHERE user = 'root' AND host = 'remote_host'; +SHOW DATABASES LIKE 'test'; + +--connection node_1 +# Prepare input: +# 1. [Empty] - current root password +# 2. n - Enable unix_socket? +# 3. n - Set root password? +# 4. n - Remove anonymous users? +# 5. Y - Disallow root login remotely? +# 6. Y - Remove test database? +# 7. Y - Reload privilege tables? + +--write_file $MYSQLTEST_VARDIR/tmp/msi_galera_input.txt + +n +n +n +Y +Y +Y +EOF + +--exec $MYSQL_SECURE_INSTALLATION --basedir=$MYSQL_BINDIR -S $MASTER_MYSOCK < $MYSQLTEST_VARDIR/tmp/msi_galera_input.txt > $MYSQLTEST_VARDIR/tmp/msi_galera.log 2>&1 + +--remove_file $MYSQLTEST_VARDIR/tmp/msi_galera_input.txt +--remove_file $MYSQLTEST_VARDIR/tmp/msi_galera.log + +--echo # Verify on node_1: remote root is gone +SELECT user, host FROM mysql.global_priv WHERE user = 'root' AND host = 'remote_host'; +--echo # Verify on node_1: test database is gone +SHOW DATABASES LIKE 'test'; + +--echo # Now verify on node_2 (replication check) +--connection node_2 +--let $wait_condition = SELECT COUNT(*) = 0 FROM mysql.global_priv WHERE user = 'root' AND host = 'remote_host' +--source include/wait_condition.inc +--echo # Verify on node_2: remote root is gone (replicated) +SELECT user, host FROM mysql.global_priv WHERE user = 'root' AND host = 'remote_host'; +--echo # Verify on node_2: test database is gone (replicated) +SHOW DATABASES LIKE 'test'; + +--source include/galera_end.inc diff --git a/scripts/mysql_secure_installation.sh b/scripts/mysql_secure_installation.sh index 58833988b1043..aecffdf2eba4b 100755 --- a/scripts/mysql_secure_installation.sh +++ b/scripts/mysql_secure_installation.sh @@ -167,10 +167,10 @@ then cannot_find_file my_print_defaults $basedir/bin $basedir/extra exit 1 fi - mysql_command=`find_in_basedir mariadb bin` + mysql_command=`find_in_basedir mariadb bin client` if test -z "$mysql_command" then - cannot_find_file mariadb $basedir/bin + cannot_find_file mariadb $basedir/bin $basedir/client exit 1 fi else @@ -316,7 +316,18 @@ set_root_password() { fi esc_pass=`basic_single_escape "$password1"` - do_query "UPDATE mysql.global_priv SET priv=json_set(priv, '$.plugin', 'mysql_native_password', '$.authentication_string', PASSWORD('$esc_pass')) WHERE User='root';" + query=" + SET @str = IFNULL((SELECT GROUP_CONCAT(CONCAT(QUOTE(User), + '@', + QUOTE(Host), + ' IDENTIFIED BY ', + '\'$esc_pass\'')) + FROM mysql.global_priv + WHERE User='root'), + '\'root\'@\'localhost\' IDENTIFIED BY \'$esc_pass\''); + SET @str = CONCAT('ALTER USER ', @str); + EXECUTE IMMEDIATE @str;" + do_query "$query" if [ $? -eq 0 ]; then echo "Password updated successfully!" echo "Reloading privilege tables.." @@ -336,7 +347,17 @@ set_root_password() { } remove_anonymous_users() { - do_query "DELETE FROM mysql.global_priv WHERE User='';" + query=" + SET @str = (SELECT IFNULL(CONCAT('DROP USER IF EXISTS ', + GROUP_CONCAT(CONCAT(QUOTE(User), + '@', + QUOTE(Host)))), + '') + FROM mysql.global_priv + WHERE User=''); + SET @str = IF(@str = '', 'DO 1', @str); + EXECUTE IMMEDIATE @str;" + do_query "$query" if [ $? -eq 0 ]; then echo " ... Success!" else @@ -348,7 +369,18 @@ remove_anonymous_users() { } remove_remote_root() { - do_query "DELETE FROM mysql.global_priv WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');" + query=" + SET @str = (SELECT IFNULL(CONCAT('DROP USER IF EXISTS ', + GROUP_CONCAT(CONCAT(QUOTE(User), + '@', + QUOTE(Host)))), + '') + FROM mysql.global_priv + WHERE User='root' AND + Host NOT IN ('localhost', '127.0.0.1', '::1')); + SET @str = IF(@str = '', 'DO 1', @str); + EXECUTE IMMEDIATE @str;" + do_query "$query" if [ $? -eq 0 ]; then echo " ... Success!" else @@ -366,12 +398,21 @@ remove_test_database() { fi echo " - Removing privileges on test database..." - do_query "DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%'" - if [ $? -eq 0 ]; then - echo " ... Success!" - else - echo " ... Failed! Not critical, keep moving..." - fi + # REVOKE explicit grants on 'test' from any remaining named users. + # Anonymous users (User='') are already removed by remove_anonymous_users() + # via DROP USER, which also removes their mysql.db rows automatically. + # FLUSH PRIVILEGES below clears any remaining stale cache entries. + query=" + SET @str = (SELECT IFNULL(CONCAT('REVOKE ALL PRIVILEGES ON \`test\`.* ', + 'FROM ', + GROUP_CONCAT(CONCAT(QUOTE(User), + '@', + QUOTE(Host)))), + 'DO 1') + FROM mysql.db + WHERE Db='test' AND User <> ''); + EXECUTE IMMEDIATE @str;" + do_query "$query" return 0 } @@ -448,7 +489,21 @@ if [ "$reply" = "n" ]; then echo " ... skipping." else emptypass=0 - do_query "UPDATE mysql.global_priv SET priv=json_set(priv, '$.password_last_changed', UNIX_TIMESTAMP(), '$.plugin', 'mysql_native_password', '$.authentication_string', 'invalid', '$.auth_or', json_array(json_object(), json_object('plugin', 'unix_socket'))) WHERE User='root';" + query=" + SET @str = IFNULL((SELECT GROUP_CONCAT(CONCAT(QUOTE(User), + '@', + QUOTE(Host), + ' IDENTIFIED VIA ', + 'mysql_native_password ', + 'USING \'invalid\' ', + 'OR unix_socket')) + FROM mysql.global_priv + WHERE User='root'), + '\'root\'@\'localhost\' IDENTIFIED VIA ' + 'mysql_native_password USING \'invalid\' OR unix_socket'); + SET @str = CONCAT('ALTER USER ', @str); + EXECUTE IMMEDIATE @str;" + do_query "$query" if [ $? -eq 0 ]; then echo "Enabled successfully!" echo "Reloading privilege tables.."