在Ubuntu上为MySQL的root用户设置空密码

自己有一台操作系统为Ubuntu 18.04.5 LTS的测试服务器,使用ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '';为 MySQL 的root用户设置了空密码。
但一直正常运行的测试应用偶尔会出现连接失败的情况,到服务器上查看后发现数据库root用户的authentication plugin变回了auth_socket,再次手动改成mysql_native_password就好了。
由于问题出现的频率不高,修复也并不麻烦,所以前几次都是手动处理了。最近又遇到了这个问题,决定找到具体原因彻底解决掉。

问题分析

为了与生产环境保持一致,测试服务器上运行的 MySQL 版本是 5.7,默认的authentication plugin还是mysql_native_password,跟 MySQL 8.0 对默认authentication plugin的调整没什么关系,还是要从服务器日志开始下手。

执行sudo systemctl status mysql直接显示的内容比较少,所以使用sudo journalctl -ru mysql查看systemd-journald记录的所有日志,-r指定按时间逆序排列,-u mysql指定服务是mysql.service。内容大致如下:

Oct 28 06:33:28 ubuntu systemd[1]: Started MySQL Community Server.
Oct 28 06:33:27 ubuntu systemd[1]: Starting MySQL Community Server...
Oct 28 06:33:16 ubuntu systemd[1]: Stopped MySQL Community Server.
Oct 28 06:33:15 ubuntu systemd[1]: Stopping MySQL Community Server...
Oct 20 07:49:10 ubuntu systemd[1]: Started MySQL Community Server.
Oct 20 07:49:09 ubuntu systemd[1]: Starting MySQL Community Server...
-- Reboot --
Oct 20 07:48:43 ubuntu systemd[1]: Stopped MySQL Community Server.
Oct 20 07:48:41 ubuntu systemd[1]: Stopping MySQL Community Server...
Oct 17 04:12:23 ubuntu systemd[1]: Started MySQL Community Server.
Oct 17 04:12:21 ubuntu systemd[1]: Starting MySQL Community Server...

日志显示在下午14:33的时候 MySQL 被重启了一次。自己用的是 zsh,所以可以通过history -i查看带有时间戳的命令历史,但在历史记录中并未发现涉及 MySQL 的命令,uptime查看服务器的运行时间也十分正常,近期没有被重启过。
将root用户改成空密码后手动重启mysql.service,重启并没有导致root用户的authentication plugin发生改变,所以推测是某些特殊原因导致了 MySQL 重启并重置了root用户的认证方式,接下来就要找到这个“特殊原因”究竟是什么。

除了systemd-journald记录的日志外,MySQL 自身在/var/log/mysql/目录下也存放有error.log,但是在里面只有正常 shutdown 和 start 的记录,下午14:33的重启和刚刚的手动重启在日志里并没有明显区别。

之前查看 Docker 的log-driver时了解到 Ubuntu 上还有一个rsyslog.service/var/log/目录下的auth.logsyslogkern.logmail.log等就是由该服务收集的,然后由logrotate定期归档整理。其中/var/log/syslog文件存储了rsyslog服务收集的所有日志,这些日志在这里记录一份后再根据相应规则写入单独的文件。

那 Ubuntu 上同时运行rsyslogsystemd-journald两个日志服务不显得很多余吗?对比了一下感觉还行,两个服务的特征及各自负责的范围有所差异,并不会一团乱麻。systemd-journald收集systemctl启动的各项服务输出内容作为日志,服务本身不需要进行配置,直接使用标准输出即可;而rsyslog如果要提取某个程序的日志,不仅要修改rsyslog的配置文件,程序本身也要使用对应的日志写入方式,它在 Ubuntu 上负责的主要就是非systemd的一些系统服务。
除此之外,man 5 journald.conf还能看到一个名称为ForwardToSyslog的选项,而且其默认值为true,所以/var/log/syslog其实是 Ubuntu 上最全的一份日志。使用sudo打开该文件,翻到对应的时间点,在 MySQL 的重启记录附近看到了这样的内容:

Oct 28 06:32:53 ubuntu systemd[1]: Starting Daily apt upgrade and clean activities...
Oct 28 06:33:14 ubuntu systemd[1]: Reloading.
Oct 28 06:33:15 ubuntu systemd[1]: Stopping MySQL Community Server...
Oct 28 06:33:16 ubuntu systemd[1]: Stopped MySQL Community Server.
Oct 28 06:33:16 ubuntu systemd[1]: Reloading.
Oct 28 06:33:27 ubuntu systemd[1]: message repeated 4 times: [ Reloading.]
Oct 28 06:33:27 ubuntu systemd[1]: Starting MySQL Community Server...
Oct 28 06:33:28 ubuntu systemd[1]: Started MySQL Community Server.
Oct 28 06:33:28 ubuntu systemd[1]: Reloading.
Oct 28 06:33:31 ubuntu systemd[1]: Started Daily apt upgrade and clean activities.

🤔 服务器还会每天自动apt upgrade?于是去查看了apt的日志文件/var/log/apt/history.log,在里面看到了以下内容:

Start-Date: 2020-10-28  06:33:14
Commandline: /usr/bin/unattended-upgrade
Upgrade: mysql-server-5.7:amd64 (5.7.31-0ubuntu0.18.04.1, 5.7.32-0ubuntu0.18.04.1), mysql-server-core-5.7:amd64 (5.7.31-0ubuntu0.18.04.1, 5.7.32-0ubuntu0.18.04.1)
End-Date: 2020-10-28  06:33:28

搜了一下unattended-upgrade,还真是 Ubuntu 从 Debian 继承过来的自动更新程序。man unattended-upgrade可以查看程序简要信息,它对自己的介绍是automatic installation of security (and other) upgrades,默认配置文件位于/etc/apt/apt.conf.d/50unattended-upgrades,过滤掉其中的注释后还剩下:

Unattended-Upgrade::Allowed-Origins {
        "${distro_id}:${distro_codename}";
        "${distro_id}:${distro_codename}-security";
        "${distro_id}ESMApps:${distro_codename}-apps-security";
        "${distro_id}ESM:${distro_codename}-infra-security";
};
Unattended-Upgrade::Package-Blacklist {
};
Unattended-Upgrade::DevRelease "false";

查找unattended-upgrade的配置说明,在 Debian Wiki 上的这个页面只找到了一些简单介绍,但在See Also部分额外提到了/usr/share/doc/unattended-upgrades/README.md.gz,执行zless /usr/share/doc/unattended-upgrades/README.md.gz直接查看文件内容,发现这个README.md里对相关配置项的介绍还是比较详细的,进一步搜索后在 Github 上也找到了对应的仓库,地址是 mvo5/unattended-upgrades

README.md中提到可以使用apt-cache policy查看相关 repository 的oanda两项,配置文件中Unattended-Upgrade::Allowed-Origins块就是一个origin:archive的列表,表示对应的 repository 启用了自动更新;Unattended-Upgrade::Package-Blacklist块是一个正则匹配的列表,表示要跳过更新的软件包;最后一个Unattended-Upgrade::DevRelease则表示在尚未发布正式 release 的 Ubuntu 上是否启用自动更新。

关于尚未发布正式 release 的 Ubuntu,在unattended-upgrades源码里可以看到依据的是distro_info.UbuntuDistroInfo().devel(result="object")distro_info是一个 Python 软件包,从 Ubuntu 软件仓库中下载得到源码后发现其依据的是/usr/share/distro-info/ubuntu.csv。最新版 Ubuntu 21.04 的 release date 是 2021-04-22,Unattended-Upgrade::DevRelease如果设为auto的话在 2021-04-01 才会开始自动更新。

执行cat /etc/lsb-release可以看到当前Ubuntu 18.04.5 LTS对应的distro_idUbuntu,对应的distro_codenamebionic,所以apt-cache policy显示的所有 repository 中除了bionic-backportsbionic-updates和自己添加的第三方 repository 外,都在每天自动更新的范围之内,不同更新类型的说明在 Ubuntu Wiki 上的这个页面有部分介绍。

回到 Mysql 这边,执行apt policy mysql-server-5.7得到:

mysql-server-5.7:
  Installed: 5.7.32-0ubuntu0.18.04.1
  Candidate: 5.7.32-0ubuntu0.18.04.1
  Version table:
 *** 5.7.32-0ubuntu0.18.04.1 500
        500 https://opentuna.cn/ubuntu bionic-updates/main amd64 Packages
        500 https://opentuna.cn/ubuntu bionic-security/main amd64 Packages
        100 /var/lib/dpkg/status
     5.7.21-1ubuntu1 500
        500 https://opentuna.cn/ubuntu bionic/main amd64 Packages

可以看到其最新版5.7.32-0ubuntu0.18.04.1是同时属于bionic-updatesbionic-security的,而bionic-security被包含于unattended-upgradeAllowed-Origins,所以每天的自动更新任务会更新到该版本。sudo apt update && apt list --upgradable查看当前需手动升级的软件包,也就是没有随着自动更新被一起升级的软件包,有docker-cenodejs,都是自己添加的第三方 repository;还有grub-common,执行apt policy grub-common得到:

grub-common:
  Installed: 2.02-2ubuntu8.17
  Candidate: 2.02-2ubuntu8.20
  Version table:
     2.02-2ubuntu8.20 500
        500 https://opentuna.cn/ubuntu bionic-updates/main amd64 Packages
 *** 2.02-2ubuntu8.17 500
        500 https://opentuna.cn/ubuntu bionic-security/main amd64 Packages
        100 /var/lib/dpkg/status
     2.02-2ubuntu8 500
        500 https://opentuna.cn/ubuntu bionic/main amd64 Packages

所以自动更新只把它更新到了bionic-security的最新版本2.02-2ubuntu8.17bionic-updates由于不在Allowed-Origins内所以不会被升级到对应版本。手动降级后再主动执行unattended-upgrade进行测试:

sudo apt install grub-common=2.02-2ubuntu8      # downgrad to 2.02-2ubuntu8
apt policy grub-common
sudo unattended-upgrade --dry-run               # show what unattended-upgrade will do
sudo unattended-upgrade                         # unattended-upgrade to 2.02-2ubuntu8.17
apt policy grub-common
sudo apt upgrade                                # upgrade to 2.02-2ubuntu8.20
apt policy grub-common

结果与预期相符,自动更新只会更新到bionic-security对应的2.02-2ubuntu8.17,手动更新才能更新到bionic-updates对应的2.02-2ubuntu8.20

手动降级mysql-server-5.7mysql-server-core-5.7后再升回来,root用户的authentication plugin果然被重置回了auth_socket,搜索相关问题后在 Ubuntu 的 Bug tracking 找到了解答,关键性内容可以执行zless /usr/share/doc/mysql-server-5.7/NEWS.Debian.gz查看:

mysql-5.7 (5.7.13-1~exp1) experimental; urgency=medium

Password behaviour when the MySQL root password is empty has changed. Packaging now enables socket authentication when the MySQL root password is empty. This means that a non-root user can’t log in as the MySQL root user with an empty password. The new logic is as follows:

  • The auth_socket plugin will be installed automatically only if it is to be activated for the root user.
  • The auth_socket plugin will be activated for the root user:
    • If you had a database before with an empty root password.
    • If you create a new database with an empty root password.
  • The auth_socket plugin will NOT be activated for the root user:
    • If you had a database before with a root password set.
    • If you create a new database with a root password set.
  • The auth_socket plugin will NOT be activated for any other user.
  • If you do not want the new behaviour, set the MySQL root password to be non-empty.

问题很清晰了,Ubuntu 每日自动更新升级mysql-server时,Mysql 检查到当前 root 用户的密码为空,就改回了auth_socket

解决办法

最开始发现问题是 Ubuntu 的每日自动更新导致的后,想直接把测试服务器上的自动更新禁用掉,毕竟在服务器上每天自动 upgrade 总是不太安心。但查完unattended-upgrades相关资料后就觉得这个自动更新还是很有必要的,而且配置文件里对更新类型有所限制,并不是简单地apt upgrade,为了保障服务器的安全还是选择把它留了下来。

那么问题该怎么解决呢?最安全的方法当然是给 root 用户指定非空密码,但那样就要去多个仓库里改涉及到的测试环境配置,为了方便我就选了另一条歪门邪道:首先创建/etc/mysql/conf.d/mysql_empty_password.cnf文件,在里面加上init_file项,然后指向一个 sql 文件,在 sql 文件中为 root 用户设置空密码。

sudo vim /etc/mysql/conf.d/mysql_empty_password.cnf
# [mysqld]
# init_file=/etc/mysql/conf.d/mysql_empty_password.sql

sudo vim /etc/mysql/conf.d/mysql_empty_password.sql
# ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '';

这样 Mysql 在每次启动时都会执行一遍mysql_empty_password.sql里的命令,升级后在下次启动时也会被改回空密码,仅用于测试服务器还是没什么问题的。

To Be Continued…

对于服务器来说,Ubuntu 继承自 Debian 的unattended-upgrades还是十分有用的,配合包管理中区分出来的不同更新类型,可以实现自动升级安全补丁,关键更新等待人工干预。如果是 Arch Linux 的 pacman 的话,就没办法实现部分更新,自己的两台 Arch Linux 服务器大概两周左右会上去手动整体更新一遍,跟unattended-upgrades比还是低了一个档次。那么同样主要用于服务器的 CentOS 有没有类似的自动更新机制呢?和 Debian 比起来又孰强孰弱呢?