分享个人 Full-Stack JavaScript 项目开发经验
MySQL 从安装到能够作为数据库服务器为应用程序提供数据,需要经过一系列数据安全和程序安全的设置。数据安全包括数据的备份、恢复、不同拓扑形式的从库复制以及数据的访问鉴权等等。不过数据安全并不是本文要分享的内容,文本主要介绍 MySQL 在安装部署到生产环境时要做的程序安全设置。文中例子是基于 Windows 操作系统的单一主库部署。虽然具体操作是针对 Windows,但多数步骤在其它操作系统中是通用的。
MySQL 在不同操作系统有许多不同的安装方式,而在 Windows 下最方便的方式是通过MySQL Installer安装。安装过程中注意选择让它作为服务器机子安装,使用推荐的密码加密方案,并让其作为 Windows 服务在开机启动时运行。
通过 MySQL Installer 安装时,它会向系统添加一个每天执行的更新任务计划。我们可以在控制面板 - 管理工具 - 任务计划程序 - 任务计划程序库 - MySQL - Installer 中找到这个任务计划,并把它设置为禁用。
mysql_secure_installation 与 mysql 是一起提供的,不需要单独安装。进入 MySQL 的安装目录:
cd C:\Program Files\MySQL\MySQL Server 8.0\bin
运行 mysql_secure_installation:
mysql_secure_installation
输入 root 密码后,可使用密码验证组件验证 root 密码强度,并确认是否需要按建议修改密码。然后我们移除用于测试的匿名用户,限制 root 账户只能通过 localhost 访问,最后是移除测试数据库和重载权限表。
在 Windows 上,MySQL 服务器的 Windows 服务默认是以名为网络服务(NETWORK SERVICE)的内置账户运行的。与本地系统帐户不同,它没有完全的系统访问权,可以提高安全性。
要了解在其它操作系统上如何设置以普通用户身份运行 MySQL,请点击这里。
数据库服务器应该只对应用程序服务器或需要同步的其它从库开放。我们应该设置系统防火墙的入站规则来限制对数据库 3306 端口的访问。在 Windows 下设置入站规则的步骤如下:
👉 开始 - 控制面板 - 防火墙 - 高级设置。
👉 点击防火墙的入站规则,查看是否已有 3306 端口的相关规则。
👉 禁用原来开放 3306 端口的规则。
👉 如果数据库只通过 localhost 访问,并且不需要其它远程从库时,则可新建一个基于 TCP 协议阻止连接本机 3306 端口的规则。
👉 如果该服务器作为一个单独的数据库服务器时,则可自定义一个仅允许远程应用程序服务器 IP 或远程从库 IP 通过 TCP 协议访问本机 3306 端口上所有应用程序和服务的连接规则。
设置完成后,我们可以使用 telnet 来测试该主机的 3306 端口访问是否受限。(Windows 系统可以在控制面板 - 程序和功能 - 启用或关闭 Windows 功能 - 勾选Telnet 客户端来启用 telnet。)
telnet <mysql ip> 3306
如果端口已经屏蔽,则会显示“无法打开到主机的连接。在端口 3306: 连接失败”。
如果你在云上使用 MySQL,云服务商也会提供防火墙/安全组设置,你只需要为特定授权对象开放仅需的端口。要了解常用协议类型端口请点击这里,了解子网划分和掩码表示方法请点击这里。
除了在系统层面限制远程 IP 对数据库 3306 端口的访问外,我们还应该在 MySQL 层面限制应用程序的数据库操作权限。root 是具有全部权限的账户,提供给应用程序使用是危险的。针对指定 IP 的应用程序,我们可以创建一个业务仅需的最小权限的账户:
CREATE USER IF NOT EXISTS 'app_account'@'localhost'
IDENTIFIED WITH mysql_native_password
AS '*123F060D426E7AD07D2AB4E209B93AA86346F691';
MySQL 8 默认使用的身份验证插件是 caching_sha2_password,但很可惜目前 npm 的 mysql 模块还未支持该插件,所以继续使用向下兼容的 mysql_native_password 插件。密码使用 AS 哈希字符串的方式设置是为了避免密码以明文方式被记录在命令历史记录文件中。由于生成哈希字符串的 PASSWORD() 函数在 MySQL 8 中已经被弃用,博主也未找到其它代替方法,只是简单的从开发机子上的 mysql.user 表中拷贝这个 authentication_string。
接着使用一条或多条 GRANT 语句为该用户授予权限。要了解更多授权语法请点击这里。
GRANT INSERT, DELETE, UPDATE, SELECT, EXECUTE, CREATE VIEW ON `db_name`.* TO 'app_account'@'localhost';
GRANT DROP ON `db_name`.`my_view` TO 'app_account'@'localhost';
如果要撤销用户的授权,可以使用 REVOKE 语句。
REVOKE DELETE ON db_name.* FROM 'app_account'@'localhost';
如果要检查用户授权,可以使用 SHOW GRANTS 语句。
SHOW GRANTS FOR 'app_account'@'localhost';
会看到还有下面这样的一条权限信息,其中的 USAGE 代表在其它地方没有权限。
GRANT USAGE ON *.* TO `app_account`@`localhost`
除了直接给用户授予访问权限外,也可以创建角色,授予角色权限,然后把角色权限授予给用户。不管用何种方式,都是为了给应用程序提供最小权限的账号,以提高安全性。
如果应用程序服务器或客户端和 MySQL 服务器之间的连接没有加密,则任何有权访问网络的人都可以查看数据。由于 MySQL 8 在加密连接失败时会返回非加密连接,所以对于远程应用程序服务器或客户端来说,应该设置用户只能通过X509连接。
所有与 X509 相关的文件都在 MySQL 安装过程中创建并保存在数据目录下。我们需要拷贝 ca.pem、client-cert.pem、client-key.pem 到客户端和应用程序中用于登录。
先在 MySQL 设置相关用户:
ALTER USER 'app_account'@'%' REQUIRE X509;
在 MySQL 8.0 Command Line Client 客户端,登录时我们需要指定 client-cert.pem、client-key.pem 的位置,如:
mysql --ssl-cert=D:\client-cert.pem --ssl-key=D:\client-key.pem -h 35.186.155.36 -u app_account -p
对于 Navicat for MySQL 这样的客户端工具,我们需要在创建连接时候指定使用 SSL,在验证选项中指定客户端密钥和证书文件路径即可。
对于应用程序,以 Node.js 的为例,数据库模块使用 knex.js,配置方式如下:
const fs = require('fs');
module.exports = require("knex")({
client: 'mysql',
connection: {
// ......
ssl: {
ca: fs.readFileSync(__dirname + '/ca.pem'),
cert: fs.readFileSync(__dirname + '/client-cert.pem'),
key: fs.readFileSync(__dirname + '/client-key.pem')
}
}
});
这些证书及密钥亦可考虑保存在环境变量 process.env 中。
(从库的复制要启用安全连接的方法与之类似。)