MySQL 这个老伙计,平时闷头干活,一旦出事就是大事。很多人对 MySQL 安全的认知还停留在“设置个密码就行”,但现实是,我见过太多数据库被脱裤、被勒索、被删库跑路,事后一查,问题往往就那么几个。这篇文章不聊虚的,我把自己在实战中踩过的坑、排查过的故障、加固过的方案,掰开揉碎讲清楚,从账号权限到传输加密,从日志审计到高危自查,都是可以直接拿去用的经验。
1. 一次让我下决心重做安全体系的凌晨告警
先说个真事。有一年我值班,凌晨两点半,主库 CPU 突然飙到 90% 多,大量慢查询堆积。登录服务器一看,processlist 里有几十个 Sleep 状态的连接,来源 IP 全是陌生的。再用 auth_string 一查,好家伙,居然有几个账号用的是 123456 这种密码。那台数据库是内部系统用的,当时图省事,bind-address 设成了 0.0.0.0,等于在内网裸奔,任何能触达这台机器的端口,都能试密码。那次之后,我花了一整个周末把所有库都加固了一遍。
这个案例说明一个核心问题:MySQL 安全最大的敌人不是攻击者多高明,而是管理员太懒。 很多人觉得“内网很安全”“就一个测试环境而已”,但内网横向渗透、测试库被扫描,都是再常见不过的路径。等出事再补救,成本会翻很多倍。下面的内容,就是我自己在加固过程中整理出的完整清单。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 账号权限体系:最小化原则落地的五个关键动作
账号权限是 MySQL 安全的第一道门,但也是被忽视得最严重的一环。下面几个动作,是我每次接手新环境必做的,顺序不能乱。
2.1 彻底处理 root 账号
MySQL 安装完,root 默认可以从任何主机登录,这非常危险。一般情况下,生产环境里 root 只应该通过本地 socket 登录,禁止 TCP 远程连接。操作方式如下:
sql复制-- 查看当前 root 账号的允许登录主机
SELECT user, host, plugin FROM mysql.user WHERE user = 'root';
-- 将 root 限制为仅本机登录
RENAME USER 'root'@'%' TO 'root'@'localhost';
如果业务上确实需要一个高权限账号,也不要直接复用 root,而是创建一个专用管理账号,并给它配置只有需要的权限。这里有个实操要点:RENAME USER 之后,原本授权给 root 的权限会跟着走,但建议重新执行一次 FLUSH PRIVILEGES;,并且顺手确认一下 MySQL 的 skip-networking 配置。如果数据库只需要本机访问,直接在 my.cnf 里加上 skip-networking 就能掐断所有 TCP 连接,最大程度压缩攻击面。
2.2 业务账号按模块拆分
很多项目为了省事,所有应用公用一个账号,这个账号还拥有 SELECT, INSERT, UPDATE, DELETE, CREATE, DROP 等各种权限。万一某个应用被注入,攻击者就能为所欲为。正确的做法是按业务模块划分账号,每个账号只给最少的权限。
例如,一个订单服务只需要操作 orders 和 order_items 两张表:
sql复制CREATE USER 'order_service'@'%' IDENTIFIED BY 'Str0ng_P@ssw0rd';
GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.orders TO 'order_service'@'%';
GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.order_items TO 'order_service'@'%';
注意,这里没有给 DDL 权限(如 CREATE, DROP, ALTER),应用即使被攻破,也只能增删改数据,不能破坏表结构。如果业务复杂度高,建议用 information_schema 结合定期巡检脚本,把超过权限范围的账号筛出来。脚本逻辑很简单:mysql.user 表里所有 Select_priv = 'Y' 且用户不是 root 的,都人工确认一遍。
2.3 密码策略:复杂度之外更要防复用
MySQL 5.7 及以上版本自带 validate_password 插件,可以强制密码长度和复杂度。在 my.cnf 里加上:
ini复制[mysqld]
plugin-load-add=validate_password.so
validate-password=ON
validate_password_policy=MEDIUM
validate_password_length=12
MEDIUM 级别会要求密码必须包含数字、小写字母、大写字母和特殊字符中的至少三种。但说实话,单靠密码复杂度挡不住撞库和泄露,更好的习惯是定期轮换 + 禁止重复使用最近几轮密码。MySQL 的 password_history 参数可以控制历史密码数量,比如设置为 5:
sql复制SET GLOBAL password_history = 5;
这招在应对等保测评时也很有用,直接少一条不满足项。
2.4 权限回收:别忽略全局和数据库级别授权
有些人在排查授权时只看表的权限,却忽略了用户可能拥有全局权限。全局权限的粒度很粗,一旦泄漏,影响面很大。我见过一个数据库管理员,为了省事,给某开发账号直接赋了 SUPER 权限,理由是“有时候需要杀进程”。后来这个账号泄露,攻击者直接执行了 SET GLOBAL sql_log_bin=OFF,导致后续所有操作都没写进 binlog,数据被改完都查不到痕迹。
清理思路很简单:
sql复制-- 查看所有拥有 SUPER 权限的账号
SELECT user, host FROM mysql.user WHERE Super_priv = 'Y';
-- 回收多余的全局权限
REVOKE SUPER ON *.* FROM 'dev_user'@'%';
如果业务确实需要某个高权限操作,宁可单独走管理平台审批,也不要长期挂在业务账号上。
2.5 连接数限制:从根源上减缓爆破
在 MySQL 里设置最大连接数,可以降低密码爆破的成功率,因为攻击者的连接会被挤掉。我一般在 my.cnf 里设置两项:
ini复制[mysqld]
max_connections = 500
max_connect_errors = 10
max_connect_errors 这个参数容易被忽视,它表示客户端连续断连超过多少次后,该客户端 IP 会被锁定,拒绝连接。这能有效遏制短时间内高频的密码试探。配合系统层的 fail2ban 或防火墙规则,可以封禁尝试多次的 IP。虽然这不能彻底防爆,但能显著提高攻击门槛。
3. 传输与落盘加密:从网络层到文件层的纵深防线
有些 DBA 觉得,MySQL 都部署在内网了,还要什么 SSL?这个想法很危险。内网里被抓包、被 ARP 欺骗的情况不是没有。只要你的应用和数据库之间传递的是明文 SQL,密码就可能在网络上裸奔。 MySQL 从 5.7 开始默认带有 SSL 支持,只是很多人没开启而已。
3.1 开启 MySQL SSL/TLS 传输加密
先检查当前是否开启了 SSL:
sql复制SHOW VARIABLES LIKE '%ssl%';
如果 have_ssl 是 DISABLED,需要在配置文件里配置证书。证书可以自签名,如果不想用自带的,也可以用 OpenSSL 生成,之后在 [mysqld] 段添加:
ini复制[mysqld]
ssl-ca=/etc/mysql/ssl/ca.pem
ssl-cert=/etc/mysql/ssl/server-cert.pem
ssl-key=/etc/mysql/ssl/server-key.pem
重启 MySQL 后,用 SHOW VARIABLES LIKE 'have_ssl' 确认是否已变成 YES。接着强制某个账号必须使用 SSL 连接,这是关键一步,因为不强制的话,客户端仍然可以走明文连接:
sql复制ALTER USER 'order_service'@'%' REQUIRE SSL;
这一步做了之后,应用连接串需要加上 useSSL=true&requireSSL=true(JDBC 为例)。否则会连接不上,很多人在这里会被卡一下。对于 PHP 的 PDO,需要在 DSN 里加 ssl-mode=REQUIRED。
3.2 连接层验证:不止是配置了证书就完事
开启 SSL 之后,我建议你再做一次抓包验证,确认真的没明文了。比如用 tcpdump 抓 3306 端口流量,然后用 Wireshark 看有没有可读的 SQL 语句。有些云厂商的数据库实例自带 SSL 开关,但如果你是自己服务器部署,最好用 mysqlbinlog 和 tcpdump 做个真实环境的抓包检查。
这里有个实操细节:如果 MySQL 是走负载均衡或数据库代理的,SSL 证书的域名或 IP 要和连接串里的地址匹配,否则证书校验会失败。很多人卡在证书验证上,就是因为连接串写的是内网 IP,证书里签发的是域名。
3.3 数据文件静态加密:库被拖走也读不了
光有传输层加密还不够。如果服务器磁盘被挂载出来,或者备份文件被直接拷贝走,数据依然是明文。 MySQL 企业版提供 InnoDB Tablespace Encryption,但社区版没有。不过社区版可以借助 Linux 的块设备加密(LUKS)或文件系统层面的加密(如 eCryptfs)来实现。
我个人的做法是:对于核心库,直接把整个数据目录放在 LUKS 加密分区上。操作系统启动时手动输入密码解锁分区,或者在服务器安全策略允许的情况下使用 TPM 自动解锁。这样即使有人把磁盘拔走,也拿不到数据。缺点是有点影响性能,实测下来大概有 5%-8% 的损耗,对于核心业务来说完全可以接受。
对于备份文件,mysqldump 出来的 SQL 是纯文本,直接落在磁盘上等于泄漏。我一般会先压缩再加密:
bash复制mysqldump -u backup_user -p mydb | gzip | openssl enc -aes-256-cbc -salt -pass pass:你的备份密码 -out /backup/mydb_$(date +%F).sql.gz.enc
解密时用 openssl enc -d 配合同样的算法和密码。有更复杂的方案用 GnuPG,但原理一样,注意密钥妥善保管,别放在同目录里。
3.4 密钥管理
谈到加密,必须提密钥管理。key 文件和数据库文件放在同一台机器甚至同一目录,等于没加密。有条件的话用单独的密钥管理服务(Vault, KMS),没条件至少也要把密钥放在独立目录,权限设为 600,并单独备份到离线设备。我在处理备份加密密钥时,会在一个离线机器上保存一份,并注明日期,防止主服务器挂了之后连备份都解不开。
4. 日志与审计:出事之后,你还有没有证据链
安全是一个系统工程,到了日志这块,很多人容易走极端:要么啥都不记,要么开着 general_log 把性能拖垮。实际上,合理的审计是个精细活儿。
4.1 general_log 的正确用法:只在故障排查时临时开
general_log 会把所有 SQL 记录到文件,性能损耗非常大,生产环境千万别常开。正确的做法是:问题排查期间临时开,查完立刻关。
生产环境排查时,可以动态开启:
sql复制SET GLOBAL general_log = 'ON';
SET GLOBAL general_log_file = '/var/log/mysql/general.log';
用完记得关:
sql复制SET GLOBAL general_log = 'OFF';
这招在我追踪诡异 bug 的时候救过好几次命。比如应用报了数据更新失败,但代码里找不到原因,打开 general log 一看,发现是另一个定时任务把所有数据改了。
4.2 MySQL 审计日志插件:更专业的选择
MySQL 企业版自带审计插件,社区版可以使用开源方案。其中比较常用的是 MariaDB Audit Plugin,它也能在 MySQL 上编译使用(具体以你的版本为准)。启用后可以记录谁在什么时间、从哪个 IP、执行了什么操作,包括 CONNECT, QUERY, LOGIN_FAILURE 等。
比如我只关心登录失败和 DDL 操作,可以这样配置:
ini复制plugin-load=AUDIT
audit_log_policy=QUERIES
audit_log_include_events=CONNECT,LOGIN_FAILURE
audit_log_exclude_commands=BEGIN,COMMIT
审计日志的价值在于:一是追溯异常操作,二是符合合规要求,三是在安全事故后用来自证清白。配置好之后,记得把审计日志的目录权限收紧,root 和专用审计账号才能读,防止攻击者进来之后把日志也删了。
4.3 慢查询日志与 SQL 注入的关联分析
慢查询日志看起来和安全没关系,但如果攻击者做基于时间盲注,会在日志里留下痕迹。比如某个 WHERE 子句里频繁出现 AND SLEEP(5),或者大量 UNION SELECT 判断。我遇到过几次明显的时间盲注尝试,都是从慢查询日志里发现的。
开启慢查询日志是一个性价比很高的动作:
ini复制[mysqld]
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
分析慢查询时,可以重点关注:
- 出现频率高的单条 SQL,且带有
SLEEP,BENCHMARK,IF(...,...)等函数调用 - 长时间没有正常业务运行的时段,却出现大量相同的查询
- 用户表、订单表等敏感表被频繁读取,但语句写法不符合常见 ORM 行为
4.4 关联监控:把 MySQL 状态纳入运维告警
安全不只是事后审计,更要靠事中感知。我通常会在监控系统里加上三个关键指标:Threads_connected、Aborted_connects、Table_locks_waited。
Threads_connected突然暴涨:可能是爬虫或攻击脚本批量建立连接。Aborted_connects大量出现:说明有人在摸密码,或者连接被异常切断。Table_locks_waited异常升高:可能有人在灌入大量写操作,意图搞乱业务。
在 MySQL 里直接看当前状态:
sql复制SHOW GLOBAL STATUS LIKE 'Aborted_connects';
SHOW GLOBAL STATUS LIKE 'Threads_connected';
配合脚本做阈值告警,能做在攻击早期发现异常。我个人习惯把 Aborted_connects 的告警阈值设为“比平时均值高 3 倍”,因为正常网络波动也会有一点失败连接,阈值设太低容易误报。
5. 备份安全与高危配置自查:最后一道防线不能有缺口
备份是数据安全的最后一道防线,但很多人备份出来了,却从来没验证过备份是否安全、能否恢复。同时,MySQL 的默认配置里有不少高危项,如果没有主动调整,就会一直处于风险敞口。
5.1 备份数据的安全保存
备份文件应该满足三个要求:加密存储、定期恢复验证、与主库隔离。我看到太多悲剧案例:备份目录和主库在同一块云盘上,主库被勒索病毒加密,备份同时也没了。
我建议备份采用“异地 + 加密 + 分权限”的组合:
- 每天凌晨用
mysqldump --single-transaction --routines --triggers导出数据,或者用XtraBackup做物理备份。 - 备份完成后立刻用
gzip压缩并用 GPG 或 OpenSSL 加密。 - 将加密后的文件实时同步到另一台独立存储或对象存储。
- 每周做一次恢复演练,在隔离环境把备份还原,确认数据可用。
恢复演练特别重要。我见过有人用某个备份软件导出了数据,但恢复时才发现备份文件头损坏。这块花费的时间,远比临时补救要少。
5.2 高危默认配置自查清单
下面列举一些我几乎每次加固都会检查的配置项,你可以对着自己环境检查一遍:
| 配置项 | 危险值 | 安全建议 |
|---|---|---|
bind-address |
0.0.0.0 |
改为内网固定 IP 或 127.0.0.1 |
local_infile |
ON |
改为 OFF,防止通过 SQL 注入读取服务器本地文件 |
skip-grant-tables |
存在该参数 | 绝对不能出现在生产环境,这是无密码登录的开关 |
old_passwords |
ON |
改为 OFF,旧密码哈希算法安全性弱 |
log_error |
未配置 | 必须配置错误日志路径,且确保日志不被任意用户读取 |
secure_file_priv |
为空 | 设置为指定目录,限制 LOAD DATA INFILE 和 SELECT ... INTO OUTFILE 的目录范围 |
secure_file_priv 这个参数特别值得多说一句。如果数据库被 SQL 注入,攻击者可以利用 SELECT ... INTO OUTFILE 把数据导出成文件,再结合 Web 目录直接拿到 shell。限制这个参数能大幅降低风险。设置方式:
ini复制[mysqld]
secure-file-priv=/var/lib/mysql-files
5.3 补丁管理与版本升级
MySQL 版本迭代会修复很多安全漏洞,像提权漏洞、认证绕过漏洞等,都是高危内容。我们需要留意官方安全公告,特别是涉及 MySQL Server 核心的补丁。
我个人建议:
- 生产环境至少用 5.7 及以上版本,官方有长期支持承诺的版本优先。
- 不要长期停在某个老版本,除非有商业支持兜底。
- 升级前在测试环境完整跑一遍业务回归测试,重点检查 SQL 模式、分组行为变化等兼容问题。
5.4 一条实用的服务器层面防线
数据库安全不只是 MySQL 内部的事,操作系统层面的加固同样重要。我一般会在服务器上配置:
bash复制# 仅允许业务网段访问 3306 端口
iptables -A INPUT -p tcp --dport 3306 -s 10.0.0.0/8 -j ACCEPT
iptables -A INPUT -p tcp --dport 3306 -j DROP
如果使用云厂商的安全组,也是一样道理,不要直接把 3306 开放到 0.0.0.0/0。很多数据库被爆破,就是因为在公网暴露了 3306。另外,SSH 登录也建议改用密钥对,禁止密码登录,否则攻击者可能不是直接打数据库,而是先拿下服务器,再连本地数据库。
5.5 我建议你立刻做的自查动作
如果你觉得自己环境可能也有问题,不用慌,按下面这几步操作一遍,能发现大部分安全隐患:
- 执行
SELECT user, host, plugin FROM mysql.user;,确认没有host='%'的 root 账号,密码插件不是auth_socket(这个代表无密码)。 - 执行
SHOW VARIABLES LIKE 'skip-grant-tables';,确认是OFF。 - 检查
my.cnf中bind-address是否为固定内网 IP。 - 确认
local_infile和secure_file_priv的配置是否符合预期。 - 查看 MySQL 错误日志目录和审计日志的权限是否为 600 或 640,所属用户是否为
mysql用户。 - 用抓包工具抽查一次应用和数据库之间的通信,确认没有明文 SQL 的传输。
这套自查流程花不了多少时间,但能帮你把绝大多数低级风险排除掉。数据库这东西,平时感觉不到它的存在,真要丢了数据或者被脱了库,那才叫晴天霹雳。安全加固不是一锤子买卖,而是习惯,每次新装一套环境就顺手过一遍,成本极低,收益极高。
