上个月帮一个创业团队排查数据库负载,没被SQL本身难住,倒是被安全配置惊到了——root密码还是root,user表里躺着匿名账号,3306端口直接挂在公网。MySQL安全加固这种事,不拖库不删库,很多人真不当回事。数据库这行干久了会发现,安全这块高手嫌简单,新手不知道从哪下手,但真正出事的团队,往往栽在最基础的配置上。这篇把我生产环境里实际动手做过的十个硬核操作一次讲清楚,覆盖账号体系、网络层、传输加密、日志审计和备份兜底,每一步都有命令、参数和踩坑心得,DBA、运维和自建库的后端同学可以直接照做。
1. 账号体系先封死:删匿名账号、上密码策略、再拆最小权限
账号是数据库的第一道门。我排查过的团队里,十个有八个问题出在账号管理上,不是密码弱,就是权限给得太大。这一节三个操作做完,等于先把手艺烂的钥匙全部换掉。
1.1 操作一:清掉匿名账号和空密码账号
很多默认安装包(尤其是某些发行版的MySQL包)初始化完会在系统表里留下匿名账号,表现形式是 user 字段为空字符串。这类账号的出现有历史原因,老版本MySQL曾用它支持一些非交互式连接场景,但到了今天基本没有正经业务会去依赖它。
先查一遍自己库里的用户清单:
sql复制SELECT user, host, authentication_string FROM mysql.user;
看到 user 为空的记录,或是 authentication_string 为空的记录,就要警惕。匿名账号意味着任何人不需要账号就可以建立会话,空密码账号则是明摆着把门敞开。清理方法:
sql复制-- 删除匿名账号
DELETE FROM mysql.user WHERE user = '';
-- 删除密码为空的账号
DELETE FROM mysql.user WHERE authentication_string = '';
-- 刷新权限缓存
FLUSH PRIVILEGES;
注意MySQL 5.7及更早版本里密码字段叫 Password,8.0改成了 authentication_string,版本不同写SQL时别照抄。清理前建议先跑一次 SHOW PROCESSLIST; 看看有没有存量连接依赖这些账号,我见过老PHP项目里真的有代码用空账号连库,删完直接把自己应用干挂了。稳妥的做法是先查清连接来源,确认无引用再删。
1.2 操作二:让密码策略插件真正工作
密码策略插件是那种“人人知道、少人安装”的组件。默认装完MySQL,你设置 123456 这种密码它根本不拦,因为校验插件压根没启用。先看当前状态:
sql复制SHOW VARIABLES LIKE 'validate_password%';
没有返回任何行,说明插件没装。MySQL 5.7安装:
sql复制INSTALL PLUGIN validate_password SONAME 'validate_password.so';
8.0版本默认启用,但如果被卸载过,需要用组件方式装回来:
sql复制INSTALL COMPONENT 'file://component_validate_password';
装完之后关键参数有四个:
| 参数 | 作用 | 建议值 |
|---|---|---|
| validate_password_policy | 密码强度等级 | MEDIUM或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在此基础上还会做字典文件匹配。生产环境我用MEDIUM起步,涉及核心支付类业务直接上STRONG。
有个坑得提醒:存量环境中直接启用插件,可能导致现有账号密码全部不符合规范,下次这个账号改密码时会被强制要求换强密码,但不会立即弹掉存量连接。不过为了不给自己埋雷,启用前先把重要账号的密码统一重置一遍。
1.3 操作三:按业务划分最小权限账号
很多团队图省事,所有应用都拿root连库,一个业务被拖库,整个实例的所有库全部沦陷。正确的做法是每个业务独立账号,只授权本业务需要的库,而且只授需要的操作权限。
创建账号的基础姿势:
sql复制CREATE USER 'app_order'@'127.0.0.1' IDENTIFIED BY '<强密码>';
GRANT SELECT, INSERT, UPDATE, DELETE ON order_db.* TO 'app_order'@'127.0.0.1';
FLUSH PRIVILEGES;
不要把 ALL PRIVILEGES 甩出去,更不要加 WITH GRANT OPTION,后者意味着这个账号可以把权限再转授给别人,等于权限失控的起点。下面这张表是我在项目里常用的权限分配参考:
| 账号类型 | 授权范围 | 建议权限 |
|---|---|---|
| 业务读写账号 | 本业务库 | SELECT, INSERT, UPDATE, DELETE |
| 只读分析账号 | 相关业务库 | SELECT |
| 备份账号 | 全局 | SELECT, RELOAD, LOCK TABLES, REPLICATION CLIENT, SHOW VIEW, PROCESS |
| 主从复制账号 | 全局 | REPLICATION SLAVE |
| 监控账号 | 全局 | SELECT, PROCESS, REPLICATION CLIENT |
备份账号和复制账号也要独立,别用业务账号顶着。每次上线前养成查授权的习惯:
sql复制SHOW GRANTS FOR 'app_order'@'127.0.0.1';
我见过某个团队给一个只负责报表查询的账号配了DROP权限,结果同事手滑一条SQL把线上表删了。最小权限不只是防外部攻击,也是在防自己人。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 网络层别裸奔:绑定地址、改端口、堵住读文件的口子
账号再强,如果MySQL直接暴露在公网上,也是天天挨扫的命。这一节三个操作做完,你的数据库基本可以从公网扫描器的视野里消失。
2.1 操作四:绑定内网监听地址,别把3306晾在公网上
MySQL默认配置在没有显式指定 bind-address 的情况下,会监听所有网卡接口,等同于公网也能触达。云服务器上如果安全组再设得宽松一点,你的数据库就成了全网扫描器眼中的活靶子。
打开配置文件,找到 [mysqld] 段:
ini复制[mysqld]
bind-address = 10.0.0.5
改成实际内网IP。如果应用和数据库在同一台机器,直接写 127.0.0.1 最安全。改完重启MySQL:
bash复制systemctl restart mysql
验证监听情况:
bash复制netstat -tlnp | grep 3306
正常应该只看到内网IP或127.0.0.1的监听记录。这里有个容易踩的坑:如果把 bind-address 设成了127.0.0.1,跨机器连接的应用会立刻连不上。改配置之前先想清楚连接方都在哪,改完第一时间用应用连接串测一遍。
云环境下,光靠 bind-address 还不够,安全组和防火墙策略必须同步收紧,只放行必要的来源IP到数据库端口,两边配合才不算裸奔。
2.2 操作五:改默认端口,配合防火墙白名单
3306是数据库端口扫描器的王牌目标,全球的扫描脚本一天到晚在扫这个端口。把端口改掉,至少能让自动化攻击工具第一轮扑空。
修改配置:
ini复制[mysqld]
port = 3307
重启后,外部连接串、监控系统、主从复制的连接配置全部要同步更新。这一步最容易把自己坑了——改完端口忘了改监控告警的端口,数据库半天连不上都不知道。
防火墙层面才是重点,用firewalld举例:
bash复制firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.0.0.0/8" port protocol="tcp" port="3307" accept'
firewall-cmd --reload
云服务器记得同步改安全组规则,只放行业务机器所在的IP段。我的建议是端口混淆和防火墙白名单一起上,端口改动是降低暴露面,防火墙才是真正的拦截手段。别指望改个端口就万事大吉,扫描器照样能通过端口指纹识别出MySQL。
2.3 操作六:关掉local_infile,收紧secure_file_priv
local_infile 这个参数控制是否允许 LOAD DATA LOCAL INFILE 从客户端读取文件并导入服务器。攻击者如果诱导你的应用连接恶意MySQL服务器,或者利用SQL注入漏洞,就能借这个特性读取客户端机器上的文件。
服务端还有一个 secure_file_priv 参数,它限制的是服务器端 LOAD_FILE() 和 LOAD DATA INFILE 可以访问的目录。如果这个值是空字符串,等于服务器上的文件能被MySQL任意读取。
建议配置:
ini复制[mysqld]
local_infile = 0
secure_file_priv = /tmp/mysql-files/
secure_file_priv 必须指定到一个专用目录,业务需要用到导入导出的功能时只允许操作这个目录。验证:
sql复制SHOW VARIABLES LIKE 'local_infile';
SHOW VARIABLES LIKE 'secure_file_priv';
注意 secure_file_priv 是只读参数,不像 local_infile 可以在会话里临时改,必须写进配置文件重启实例。如果业务确实有导入需求,可以在专用目录下建好子目录,再把需要导入的文件放进去,不要图省事把值设成空字符串,那等于没关。
3. 传输与对象防提权:SSL加密、UDF清理、存储过程权限收口
前两节解决的是“谁能进来”,这一节解决的是“数据在路上安不安全”和“进来的人能不能顺着后门提权”。很多团队在这块完全空白,出了问题才发现连排查方向都没有。
3.1 操作七:启用SSL/TLS传输加密,让抓包变成白费功夫
默认情况下,MySQL客户端和服务器之间的通信是明文传输。账号密码、业务数据、SQL语句全裸在路上走,局域网里只要有人做了抓包,等于白送。
MySQL 5.7和8.0在初始化数据目录时一般会自动生成自签名SSL证书并启用SSL支持。先看看当前实际状态:
sql复制SHOW VARIABLES LIKE '%ssl%';
确认 have_ssl 是 YES。但从“支持SSL”到“强制SSL”是两码事,必须从账号侧强制:
sql复制ALTER USER 'app_order'@'127.0.0.1' REQUIRE SSL;
这样该账号的所有连接都必须是加密连接,不加密直接拒绝。客户端连接时也要对应开启SSL模式:
bash复制mysql --ssl-mode=REQUIRED -u app_order -p
自签名证书的问题是客户端无法验证服务端身份,中间人攻击依然存在。生产环境建议用企业内部CA或正规证书签发机构的证书,配置到以下参数:
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
配置好之后用 \s 查看连接状态,看到 SSL: Cipher in use is ... 说明确实加密了。我实测过,SSL对OLTP小查询的性能影响一般在个位数百分比,绝大多数业务无感,别拿性能当借口不开。
还有主从复制链路,如果复制账号没配SSL,主库到从库的数据仍然是明文。检查从库状态:
sql复制SHOW SLAVE STATUS\G
留意复制连接方式,生产环境建议主从也走SSL。
3.2 操作八:排查UDF、存储过程和触发器里埋着的提权路径
UDF提权是老牌攻击手法了。MySQL允许通过自定义函数方式扩展功能,攻击者如果能把一个恶意的 .so 文件写进插件目录,再创建对应的自定义函数,就能用MySQL进程的权限执行系统命令。
先检查插件目录和目录权限:
sql复制SHOW VARIABLES LIKE 'plugin_dir';
确认该目录的属主是mysql用户,且mysql系统用户、业务账号都没有向该目录写入文件的途径。再检查是否已经有可疑的自定义函数:
sql复制SELECT * FROM mysql.func;
正常业务基本不会有UDF函数,发现可疑记录直接删除并排查插件目录里多出来的文件。
存储过程和触发器是另一条容易被忽视的路。如果业务账号有 CREATE ROUTINE 权限,它可以创建 SQL SECURITY DEFINER 的存储过程,别人调用时以定义者的权限执行,定义者如果是root,那就是直接提权。检查现有对象:
sql复制SELECT routine_schema, routine_name, security_type
FROM information_schema.routines
WHERE security_type = 'DEFINER';
SELECT trigger_schema, trigger_name, definer
FROM information_schema.triggers;
最小权限原则下,普通业务账号不该有 CREATE ROUTINE、ALTER ROUTINE、CREATE TRIGGER、EVENT 这些权限。默认的test库如果存在,也一并删掉,它历史上来就是给匿名账号留的临时操作空间:
sql复制DROP DATABASE IF EXISTS test;
这一步做完,相当于把数据库内部最常用的几条提权路径都堵上了。
4. 日志与审计:真出事的时候,你得能说清楚谁干了什么
每次帮人排查安全事件,最无奈的就是数据库日志一片空白。安全不是“不出事就行”,而是出事之后能还原过程、定位漏洞、追溯责任。日志开好,事情就成功了一半。
4.1 操作九:错误日志、慢查询、通用日志、binlog四件套组合
错误日志是基础中的基础,记录启动关闭、连接异常、权限问题等信息。配置:
ini复制[mysqld]
log_error = /var/log/mysql/error.log
慢查询日志不只用来调优,攻击者批量拖库时,往往伴随大量超大范围查询,慢查询日志里会留下痕迹。配置为记录超过1秒的语句:
ini复制[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
通用日志 general_log 会记录每一条SQL,信息最全但开销也最大,生产环境不建议常开。适合在安全事件排查期间临时开几分钟,用完立刻关:
sql复制SET GLOBAL general_log = 'ON';
-- 排查完成后马上关闭
SET GLOBAL general_log = 'OFF';
binlog更要重视。它记录所有数据变更,既是数据恢复的最后依靠,也是判断“数据什么时候被谁改过”的核心依据。配置建议:
ini复制[mysqld]
log_bin = /var/log/mysql/mysql-bin
binlog_format = ROW
binlog保留周期,5.7用 expire_logs_days,8.0改成了 binlog_expire_logs_seconds:
ini复制# 5.7
expire_logs_days = 15
# 8.0
binlog_expire_logs_seconds = 1296000
日志审计还有一个常见选择,就是接入审计插件。MySQL企业版自带审计功能,开源场景可以用McAfee审计插件或MariaDB审计插件,能记录谁在什么时间执行了什么SQL,比手工翻日志高效得多。我在夜维场景下主要靠binlog定位数据变更,配合审计插件确认账号行为。
日志文件本身也要防篡改,不要把日志放在web服务可访问的目录,用logrotate做轮转,磁盘空间监控跟上。日志写满磁盘导致数据库宕机这种事,比被攻击还常见。
5. 兜底工程:备份要能恢复,文件权限和补丁是最后防线
前面的操作是防守,这最后一节是底线。数据库被攻破不可怕,可怕的是被攻破之后数据全没了,连重来的机会都没有。备份能恢复、文件权限封死、补丁及时打,这三件事做完,才算真正心里有底。
5.1 操作十:备份不是“有备份”,而是“能恢复”
我遇到过不止一个团队,备份脚本老老实实跑了大半年,真到恢复的时候才发现参数写错,备份文件根本不能用。备份这件事,核心指标只有一个:能不能恢复,多久能恢复。
中小型实例用 mysqldump 做逻辑备份就够了,注意加 --single-transaction 参数,保证InnoDB表备份期间数据的一致性,同时不影响线上读写:
bash复制mysqldump \
-u备份账号 -p \
--single-transaction \
--routines \
--triggers \
--events \
--set-gtid-purged=OFF \
order_db > /backup/order_db_$(date +%F).sql
实例数据量大、备份窗口短的场景,换物理备份工具,比如Percona XtraBackup,备份速度和恢复速度都比逻辑备份快一个量级。
备份策略上,我的做法是全量+增量配合:
| 频次 | 内容 | 保留时间 |
|---|---|---|
| 每天凌晨 | 全量备份 | 30天 |
| 实时 | binlog增量 | 15天 |
关键在恢复演练。每个季度至少做一次完整恢复,把备份拉到测试实例上,对比备份时间和当前数据,确认数据可用。我自己的习惯是恢复完再抽查几条关键表的数据,不能只看到“恢复成功”就跑。
备份文件本身也是敏感数据,里面是全部明文业务数据,权限要限制,传到对象存储时设好访问策略,别把备份文件放到公开可读的桶里。
5.2 文件权限和补丁补管理:最后一毫米的防御
MySQL的配置文件 my.cnf 里经常藏着复制账号密码、数据库口令,这类文件权限必须收紧:
bash复制chown mysql:mysql /etc/mysql/my.cnf
chmod 600 /etc/mysql/my.cnf
数据目录、binlog目录、日志目录的属主和权限也检查一遍,避免其他系统用户能直接读文件拿到数据。命令:
bash复制chown -R mysql:mysql /var/lib/mysql
chmod 750 /var/lib/mysql
补丁这块容易被忽略。很多团队数据库装完就不动了,版本还停留在官方早已停止维护的版本。这里说的不只是大版本升级,小版本也会修安全漏洞。新版装之前先在测试环境验证一遍兼容性,没问题再上生产。
bash复制mysql --version
新项目直接用8.0,5.7还有存量业务的话尽快规划升级,越拖风险越高。
关于文件权限,我碰过一个案例,为了省事把整个/var/lib/mysql目录设成了777,结果web服务的账号都能读数据文件,等于把数据库整个裸给了攻击者。这类细节平时没人注意,出问题就是大事。
最后说点实际体会。我见过太多团队把这十个操作做完就再也不管,结果半年后配置漂移、权限又乱成一锅粥。安全加固不是一次性的冲刺,而是日常的重复劳动。我的做法是建一个checklist放在发布流程里,每次上线前过一遍,另外每季度用脚本扫一遍用户表和关键配置项。你如果不想手工做,也可以把 SHOW GRANTS、SHOW VARIABLES 这些查询攒成一个巡检脚本,定时发到通知群里。数据库安全拼的不是谁懂的多,而是谁坚持得久。
