From e36354e1c1c962ea49e891a4dbe9bac06bf21f14 Mon Sep 17 00:00:00 2001 From: Thomas Gerbet Date: Fri, 12 Jun 2026 23:38:41 +0200 Subject: [PATCH] nixos/mysql: fix default MySQL/Percona Server insecure authentication Existing deployments using MySQL or Percona Server with `services.mysql` do not restrict authentication to the root@localhost user allowing any user on the system to log in without a password. With this changes: * new deployments will use the `auth_socket` authentication by default to only allow the local root user to log in as the MySQL root user * existing deployments with a stateVersion > 26.05 will also be moved to the `auth_socket` authentication * existing deployments with a stateVersion <= 26.05 have a warning logged when the local authentication seems to be open Users can disable this behavior by setting `services.mysql.secureSuperUserByDefault` to `false`. (cherry picked from commit 1d6947077ee22cd86c0fb7df112f8cfa211ab374) --- nixos/modules/services/databases/mysql.nix | 44 ++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/mysql/mysql-secure-root.nix | 94 ++++++++++++++++++++++ pkgs/by-name/my/mysql84/package.nix | 5 ++ 4 files changed, 144 insertions(+) create mode 100644 nixos/tests/mysql/mysql-secure-root.nix diff --git a/nixos/modules/services/databases/mysql.nix b/nixos/modules/services/databases/mysql.nix index 4a196efc7758..c31b6b7ebbf8 100644 --- a/nixos/modules/services/databases/mysql.nix +++ b/nixos/modules/services/databases/mysql.nix @@ -293,6 +293,19 @@ in ''; }; + 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 [ @@ -411,6 +424,10 @@ in 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 [ @@ -571,6 +588,7 @@ in 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 '' @@ -644,6 +662,11 @@ in ) | ${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 + ''} + ${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, @@ -654,6 +677,27 @@ in 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: '' diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index b838d2d8cff4..07af38660bab 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1043,6 +1043,7 @@ in mysql-autobackup = handleTest ./mysql/mysql-autobackup.nix { }; mysql-backup = handleTest ./mysql/mysql-backup.nix { }; mysql-replication = handleTest ./mysql/mysql-replication.nix { }; + mysql-secure-root = handleTest ./mysql/mysql-secure-root.nix { }; n8n = runTest ./n8n.nix; nagios = runTestOn [ "x86_64-linux" "aarch64-linux" ] ./nagios.nix; nar-serve = runTest ./nar-serve.nix; diff --git a/nixos/tests/mysql/mysql-secure-root.nix b/nixos/tests/mysql/mysql-secure-root.nix new file mode 100644 index 000000000000..faaf1bb3955e --- /dev/null +++ b/nixos/tests/mysql/mysql-secure-root.nix @@ -0,0 +1,94 @@ +{ + system ? builtins.currentSystem, + config ? { }, + pkgs ? import ../../.. { inherit system config; }, + lib ? pkgs.lib, +}: + +let + makeTest = import ./../make-test-python.nix; + inherit (import ./common.nix { inherit pkgs lib; }) + mysqlPackages + ; + + makeSecureRootTest = + { + package, + name ? "mysql_secure_root_" + (builtins.replaceStrings [ "-" "." ] [ "_" "" ] package.pname), + }: + makeTest { + inherit name; + + nodes.${name} = { pkgs, ... }: { + services.mysql = { + enable = true; + package = package; + }; + }; + + testScript = '' + start_all() + + machine = ${name} + machine.wait_for_unit("mysql") + + # Verify that non-root user cannot connect as root + machine.fail("sudo -u nobody mysql -u root -e 'SELECT 1;' 2>&1") + + # Verify that system root can connect as root via socket + machine.succeed("mysql -u root -e 'SELECT 1;'") + + # Verify that root@localhost has auth_socket plugin + machine.succeed("[ \"$(mysql -u root -N -e \"SELECT plugin FROM mysql.user WHERE user = 'root' AND host = 'localhost';\")\" = \"auth_socket\" ]") + + # Test service restart - verify it still works + machine.succeed("systemctl restart mysql") + machine.wait_for_unit("mysql") + + # After restart, verify non-root user still cannot connect as root + machine.fail("sudo -u nobody mysql -u root -e 'SELECT 1;' 2>&1") + + # After restart, verify system root can still connect + machine.succeed("mysql -u root -e 'SELECT 1;'") + + # After restart, verify root@localhost still has auth_socket + machine.succeed("[ \"$(mysql -u root -N -e \"SELECT plugin FROM mysql.user WHERE user = 'root' AND host = 'localhost';\")\" = \"auth_socket\" ]") + ''; + }; + + makeInsecureRootTest = + { + package, + name ? "mysql_insecure_root_" + (builtins.replaceStrings [ "-" "." ] [ "_" "" ] package.pname), + }: + makeTest { + inherit name; + + nodes.${name} = { pkgs, ... }: { + services.mysql = { + enable = true; + package = package; + secureSuperUserByDefault = false; + }; + }; + + testScript = '' + start_all() + + machine = ${name} + machine.wait_for_unit("mysql") + + # With secureRootByDefault = false, anyone can connect as root (default --initialize-insecure behavior) + machine.succeed("sudo -u nobody mysql -u root -e 'SELECT 1;' 2>&1") + ''; + }; + +in +{ + "secure-by-default" = lib.mapAttrs ( + _: package: makeSecureRootTest { inherit package; } + ) mysqlPackages; + "can-be-insecure" = lib.mapAttrs ( + _: package: makeInsecureRootTest { inherit package; } + ) mysqlPackages; +} diff --git a/pkgs/by-name/my/mysql84/package.nix b/pkgs/by-name/my/mysql84/package.nix index 5eed4c7b57f0..129355bfb336 100644 --- a/pkgs/by-name/my/mysql84/package.nix +++ b/pkgs/by-name/my/mysql84/package.nix @@ -23,6 +23,7 @@ libtirpc, rpcsvc-proto, curl, + nixosTests, }: stdenv.mkDerivation (finalAttrs: { @@ -111,6 +112,10 @@ stdenv.mkDerivation (finalAttrs: { connector-c = finalAttrs.finalPackage; server = finalAttrs.finalPackage; mysqlVersion = lib.versions.majorMinor finalAttrs.version; + tests.mysql-secure-root-by-default = + nixosTests.mysql-secure-root.secure-by-default."mysql${lib.versions.major finalAttrs.version}${lib.versions.minor finalAttrs.version}"; + tests.mysql-root-can-be-kept-insecure = + nixosTests.mysql-secure-root.can-be-insecure."mysql${lib.versions.major finalAttrs.version}${lib.versions.minor finalAttrs.version}"; }; meta = {