这几年被拉去处理过不少MySQL安全问题,坦白讲,真正让人头疼的从来不是数据库本身多脆弱,而是账号太乱、端口太开放、备份不加密、日志不归档这类基础问题。我整理了一份MySQL安全加固的十个硬核操作清单,全部来自生产环境的实战,不是让你“看完知道有这回事”,而是每条都给了能直接执行的命令、参数和踩坑提醒。
这篇内容适合两类人:一是刚接手一套线上MySQL,想做一次安全基线整理的DBA或运维;二是后端开发想了解数据库加固到底在加什么,避免写出一个权限比DBA还大的应用账号。按顺序操作,绝大多数常规安全隐患都能被挡住,而且不会影响正常业务。
1. 账号与口令收敛:先把最容易撞开的门锁换掉
1.1 第一招:密码复杂度校验必须在建号时就生效
很多人以为安全加固是给现有账号换个强密码就完事,但真正的问题是“下一个新账号”有没有被规则约束。MySQL 8.0默认不会强制密码复杂度,业务方自己建号时写个123456也能通过,这种后门防不住。
MySQL 8.0和5.7的安装方式不一样,这点非常容易踩坑:
- MySQL 5.7:用插件机制,
INSTALL PLUGIN validate_password SONAME 'validate_password.so'; - MySQL 8.0:用组件机制,
INSTALL COMPONENT 'file://component_validate_password';
装完以后先看策略参数:
sql复制SHOW VARIABLES LIKE 'validate_password%';
我推荐生产环境至少是这个档位:
ini复制validate_password.policy = STRONG
validate_password.length = 12
validate_password.mixed_case_count = 1
validate_password.number_count = 1
validate_password.special_char_count = 1
LOW策略只检查长度,MEDIUM会检查数字、大小写和特殊字符,STRONG还会检查字典文件。既然要上加固,就别只开个LOW欺骗自己。
这里有一个很多人不知道的坑:安装组件后,已存在的弱密码账号不会被强制失效。组件只约束后续的CREATE USER和ALTER USER。你需要手动把存量弱密码账号找出来,让它们强制过期:
sql复制SELECT user, host, password_expired
FROM mysql.user
WHERE authentication_string = '' OR password_expired = 'N';
执行:
sql复制ALTER USER 'someuser'@'%' PASSWORD EXPIRE;
这样这个账号下一次登录时必须先改密码,否则什么都干不了。我见过不少团队装完组件就宣布“密码策略已启用”,结果一查线上还有几十个空密码或者明文弱密码账号,等于白装。
1.2 第二招:清掉匿名账号和“空密码”残留
MySQL安装或者搬库之后,最容易残留三类账号:用户名是空字符串的匿名账号、authentication_string为空字符串的空密码账号、以及user表里仍然允许%任意主机登录的root账号。
先做一次全面体检:
sql复制SELECT user, host, plugin, authentication_string, account_locked, password_expired
FROM mysql.user
ORDER BY user, host;
重点看几类异常:
| 异常类型 | 判断方式 | 风险 |
|---|---|---|
| 匿名账号 | user字段是空字符串 |
权限匹配可能绕过真实账号,导致权限判定混乱 |
| 空密码账号 | authentication_string为空字符串 |
任何人都能免密登录,典型的隐形后门 |
| 全开放账号 | host字段是%且不是业务明确要求 |
攻击面无限放大 |
| 锁定账号 | account_locked为Y但还在被连接 |
程序配置残留,说明连接配置有历史包袱 |
对于匿名账号,直接删:
sql复制DROP USER IF EXISTS ''@'localhost';
DROP USER IF EXISTS ''@'hostname';
如果只用一条命令处理完,官方提供的mysql_secure_installation脚本也能在初始化阶段帮忙做掉几件事:移除匿名账号、禁止root远程登录、删除默认创建的test库。但注意这个脚本在不同发行版里交互方式不同,不能盲目执行,至少要确认它不会顺手把root密码改掉。
删除test库也别忽略。test库在默认权限配置下常常允许本地用户随意读写,留着它没任何好处。
1.3 第三招:把root限到本机,日常管理另建专用账号
root账号是加固中的大头。最理想的状态是:数据库root只出现在本机运维场景,所有远程管理都通过一个具备最小必要权限的专用账号完成。
先看root现在长什么样:
sql复制SELECT user, host FROM mysql.user WHERE user = 'root';
如果结果里有root@'%',直接删:
sql复制DROP USER 'root'@'%';
保留root@localhost用于本机应急登录就够了。像Ubuntu自带的MySQL 8.0,root默认走auth_socket插件,本机root用户连数据库不需要密码,这种设计其实是把本机系统账号和数据库管理员身份绑定,作为应急入口是合理的。如果你远程要用root,正确做法不是给它开放网络访问,而是登跳板机后sudo mysql再执行操作。
日常运维账号单独建,不给超纲的权限:
sql复制CREATE USER 'opsadmin'@'192.168.10.%' IDENTIFIED BY '这里写一个足够长的强密码';
GRANT ALL PRIVILEGES ON *.* TO 'opsadmin'@'192.168.10.%';
这里有一个取舍:真正意义上的DBA确实需要跨库管理,所以给ALL PRIVILEGES能理解。但不要随手带WITH GRANT OPTION。如果账号能把自己的权限授权给别人,一旦这个账号被攻破,整个实例的权限体系就彻底失效了。我一般建议平时管理不需要开GRANT OPTION,只有专门的账号管理流程才需要,而且那种账号还要结合堡垒机来用。
顺便检查一下当前到底有多少账号带着授权能力:
sql复制SELECT user, host, Grant_priv
FROM mysql.user
WHERE Grant_priv = 'Y';
这个列表里的每一个账号都相当于半个root,数量应该控制在个位数。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 权限与网络边界:拉小攻击者能活动的半径
2.1 第四招:用角色拆分权限,开发账号与DBA账号彻底分离
MySQL从8.0开始支持角色,很多人只把它当成一个“逻辑分组”功能,实际上它是权限收敛的重要工具。没有角色的时候,DBA给开发开权限都是复制粘贴一堆GRANT语句,时间一长,谁也不清楚哪个账号到底拥有哪些权限。
我用角色一般会拆成读取、写入、DDL三类:
sql复制CREATE ROLE IF NOT EXISTS 'app_read';
CREATE ROLE IF NOT EXISTS 'app_write';
CREATE ROLE IF NOT EXISTS 'app_ddl';
GRANT SELECT ON `appdb`.* TO 'app_read';
GRANT INSERT, UPDATE, DELETE ON `appdb`.* TO 'app_write';
GRANT CREATE, ALTER, INDEX, DROP, REFERENCES ON `appdb`.* TO 'app_ddl';
把角色授予业务账号:
sql复制CREATE USER 'app_service'@'10.0.1.5' IDENTIFIED BY '强密码';
GRANT 'app_read', 'app_write' TO 'app_service'@'10.0.1.5';
这里有一个角色机制的坑:给用户授权角色之后,角色默认不会激活。用户登录后需要手动执行SET ROLE 'app_read'才会生效,如果程序侧没有执行这条语句,权限会一直不生效,导致应用莫名其妙报无权限。解决办法是全局开启自动激活:
sql复制SET GLOBAL activate_all_roles_on_login = ON;
配置文件里也写上activate_all_roles_on_login = ON,不然重启又回去了。
开发同学如果需要临时排查数据,不应该让他们直接连生产库执行SQL。可以按需开通一个只读账号并指定IP来源,比如只允许从跳板机所在网段访问,用完即回收。权限越小,业务出问题时能造成的破坏越小。
2.2 第五招:回收FILE、SUPER、PROCESS类高危权限
MySQL权限模型里有一部分权限看起来不起眼,一旦被滥用就非常致命。重点盯三类:
| 权限 | 危害路径 | 生产建议 |
|---|---|---|
| FILE | 可以LOAD_FILE()读服务器文件,也可以SELECT ... INTO OUTFILE写文件 |
业务账号一律不授,特殊需求走专用备份/ETL账号 |
| SUPER/PROCESS | 能看到其他会话完整SQL、能影响复制线程,在高版本里相当于半个管理员 | 业务账号一律不授 |
| GRANT OPTION | 能把权限传播给其他账号 | 只保留极少数管理账号 |
检查当前哪些账号持有这些权限:
sql复制SELECT user, host, File_priv, Super_priv, Process_priv, Grant_priv
FROM mysql.user
WHERE File_priv = 'Y'
OR Super_priv = 'Y'
OR Process_priv = 'Y'
OR Grant_priv = 'Y';
回收举例:
sql复制REVOKE FILE ON *.* FROM 'someapp'@'%';
REVOKE PROCESS ON *.* FROM 'someapp'@'%';
注意一点,如果某个服务确实需要PROCESS权限去看正在运行的SQL,这通常说明排查手段已经出了问题。对线上实例而言,更合适的做法是用performance_schema来观察会话和SQL,而不是靠PROCESS权限去“看别人在跑什么”。
MySQL 8.0还引入了SYSTEM_USER权限,它管理的是一批系统账号。如果不小心把SYSTEM_USER授予了普通账号,就可能出现普通账号去操作管理员账号的情况。所以8.0环境里还要额外检查:
sql复制SELECT user, host FROM mysql.user WHERE User_attributes->>'$.metadata' IS NOT NULL;
权限清理这件事不是一锤子买卖。业务每上线一个模块,都可能有人申请权限,如果不定期检查,账号权限一定会慢慢膨胀。三个月做一次权限巡检不算频繁,只能说刚好。
2.3 第六招:端口不能被全互联网访问,bind-address和防火墙都要管
先看当前实例监听在哪里:
sql复制SHOW VARIABLES LIKE 'bind_address';
如果输出是0.0.0.0或者*,同时ECS安全组还放了3306口,那基本等于把数据库裸奔在公网。现在扫描工具满天飞,3306被扫到是早晚的事。
修改监听地址,最稳妥的是只监听内网IP:
ini复制
