我先说个真事。前几年做数据库巡检有个客户的核心业务库root账号密码是“root123”3306端口直接对公网开放我登录上去一查binlog日志里明晃晃躺着几十条删表记录——不是误操作是被人连进来删了又建来回折腾了好几次。排查到最后发现连防火墙都没拦这个端口。这种事听多了你会发现一个规律真正被“高级攻击”打穿的极少绝大多数问题都出在基础项上——弱口令、权限过宽、端口裸奔、日志没开、补丁不更新。所以后来我给自己定了个规矩所有新接手的MySQL实例不管跑的是什么业务先照着安全基线过一遍没过关不许上线。这份“MySQL安全加固十大硬核操作”大纲就是我在反复踩坑、反复复盘之后沉淀下来的清单。它的适用对象很明确自己搭库的开发者、刚接手数据库的运维、以及不想等出事再补救的后端团队。每一条都能直接落到命令行上执行不需要额外采购商业产品也不需要重启业务大部分操作可以在线完成。1. 为什么是这十项操作安全加固的整体思路先别急着复制命令你得知道这十项是怎么选出来的。MySQL安全加固说到底就干三件事让不该进来的人进不来让进来的人干不了不该干的事让干了的事留痕且可恢复。对应到执行层面就是要同时管住账号、管住权限、管住网络、管住运行环境、管住日志与备份。1.1 选型标准低成本、低影响、可验证我整理清单时有三个硬性标准。第一必须不需要停库能在线执行的绝不放进停机窗口第二必须有明确的验证方式做完能立刻看到效果不是“改了配置感觉安全了点”这种玄学第三必须覆盖“事前、事中、事后”全链路既有预防手段也有追溯手段。这十条具体是这样分布的第1到3条管账户和权限解决“谁能用、能用什么”第4到7条管网络和运行环境解决“从哪里进、以什么身份跑”第8到10条管审计、备份和版本解决“干了什么、出了问题怎么恢复、已知漏洞怎么补”。如果你时间紧张只能做三件事我建议优先做权限最小化、网络暴露面收缩、审计日志开启——这三项防住了大部分基础攻击路径就被堵死了。1.2 纵深防御的核心理念MySQL加固从来不是“装个安全软件”就完事而是多层防线叠加。就算攻击者拿到了应用账号他不能登录系统就算他能登录系统他发现没有额外权限就算他有权限他改不了数据目录的文件就算他删了数据你有加密备份能恢复。每一层都让攻击成本翻一倍这才是加固的意义。所以你会看到下面的操作里没有一条是“装个防篡改组件”这种一锤子买卖更多是把MySQL自身的能力用到位自带的密码策略、自带SSL、自带binlog以及操作系统的权限机制。把这些基础能力吃透比到处找第三方防护工具靠谱得多。2. 账户与权限层面先把门口的人清干净这个维度最容易出成效也最容易被人忽视。我见过太多库光mysql.user表里就躺着十几个没人记得的账号密码还是默认的。这个环节动手风险最低、收益最高。2.1 操作一清理危险账户干掉匿名和重复账号第一步永远是先查账。连上MySQL执行下面的SQL把mysql.user表里的所有账号拉出来看一遍SELECT User, Host, authentication_string, plugin FROM mysql.user;重点排查三类账号User为空的匿名账号Host为‘%’的任意主机远程账号以及Host为localhost但长期未登录的僵尸账号。匿名账号是历史遗留问题MySQL早期安装或某些初始化脚本会创建它意味着任何人不需要密码就能尝试连接虽然默认只能连库不能操作但放在生产环境里就是个隐患。处理方式分两种能确定没用的直接删DROP USER localhost; DROP USER %;不确定有没有业务在用的账号不要急着删先把账号锁住观察一段时间ALTER USER someuser% ACCOUNT LOCK;锁住之后跑一周业务如果应用没有报连接错误说明这个账号可以彻底删除。注意执行DROP后连接会立刻断开如果该账号正在被业务使用影响是实时的所以锁账号观察比直接删除更稳妥。注意不要重复创建同名的同Host账号。MySQL判断账号是User加Host组合同一个User配多个Host并不是错误但会增加管理复杂度。建议业务账号统一用一个Host规则比如应用服务器网段尽量少用‘%’。2.2 操作二用密码策略组件卡住弱口令清理完账户接着处理密码质量。MySQL 5.7提供validate_password插件8.0起升级为组件。我见过有人问我“我密码设得很复杂为什么还要装这个”实际上密码策略组件不只是校验强度它还能强制定期更换、拒绝复用是防止撞库攻击的关键防线。先查当前状态SHOW VARIABLES LIKE validate_password%;没启用的话MySQL 8.0用组件方式安装INSTALL COMPONENT file://component_validate_password;5.7及兼容版本用插件方式INSTALL PLUGIN validate_password SONAME validate_password.so;装完之后的参数设置推荐生产环境按这个基线配置SET GLOBAL validate_password.policy 2; SET GLOBAL validate_password.length 12; SET GLOBAL validate_password.mixed_case_count 1; SET GLOBAL validate_password.number_count 1; SET GLOBAL validate_password.special_char_count 1;policy设为2表示STRONG会在LOW、MEDIUM基础上额外检查字典文件。length设为12配合大小写、数字、特殊字符各至少1个这个强度对于内部业务系统足够又不至于让人记不住。注意这些是运行时参数如果要永久生效还需要同步写进my.cnf的[mysqld]段或者用SET PERSIST命令SET PERSIST validate_password.policy 2;SET PERSIST会同步写入mysqld-auto.cnf重启后依然生效比手动改配置文件更不容易出错。有个坑要提醒密码策略组件只对新设置的密码生效对存量账号的旧密码不生效。所以装完组件之后还要把所有账号密码轮换一遍ALTER USER appuser10.0.0.% IDENTIFIED BY 新密码;轮换密码时如果担心影响业务可以提前用mysql_config_editor把客户端登录信息存起来或者让应用侧先切到新密码再执行ALTER。2.3 操作三最小化授权别再用ALL PRIVILEGES权限这块我只说一句能不开的权限坚决不开能用库级权限别用全局权限能用表级权限别用库级权限。很多应用部署文档为了省事直接让运维执行GRANT ALL PRIVILEGES ON.这等于给应用账号开了个“超级vip通行证”一旦账号被注入或者泄露整个库都保不住。正确的做法是先查再改。查看现有账号的权限SHOW GRANTS FOR appuser10.0.0.%;一般业务账号只需要增删改查那就只给这四类权限GRANT SELECT, INSERT, UPDATE, DELETE ON businessdb.* TO appuser10.0.0.%;如果应用需要执行DDL比如建表、改表结构再单独加ALTER、CREATE、INDEX等权限而不是给ALLGRANT ALTER, CREATE, INDEX ON businessdb.* TO appuser10.0.0.%;回收多余权限用REVOKEREVOKE DELETE ON businessdb.* FROM appuser10.0.0.%; REVOKE SUPER ON *.* FROM appuser10.0.0.%;同一个账号在多个Host下都有记录REVOKE时要注意写全Host否则可能出现“权限没变”的错觉。另外MySQL 8.0中GRANT不能再隐式创建用户了必须先CREATE USER再GRANT这一点和5.7行为不同迁移时要留意。实战中我还会定期跑一条SQL筛出带全局管理权限的账号人工核对是否必要SELECT User, Host, Super_priv, Create_user_priv, Grant_priv FROM mysql.user WHERE Super_privY OR Create_user_privY OR Grant_privY;建议每个月跑一次结果发到工作群让大家确认。权限最小化不是一次性的业务变更了权限就要跟着变这是个持续动作。3. 网络与运行环境层面把入口关到最小账户和权限管好了接下来处理网络边界和运行环境。这一层的操作直接影响MySQL的可达性很多系统被入侵就是因为服务暴露在了不该暴露的地方。3.1 操作四绑定监听地址禁止root远程登录MySQL默认监听所有网卡也就是bind_address为0.0.0.0或*这意味着只要网络通任何机器都能尝试连接3306。生产环境必须把监听地址收缩到业务需要的范围。编辑my.cnf在[mysqld]段下设置[mysqld] bind-address 10.0.1.10这里的IP要写MySQL服务器的内网IP不要写0.0.0.0也不要写公网IP。如果应用和数据库在同一台机器直接写127.0.0.1最安全。改完重启MySQL生效systemctl restart mysqld重启后检查SHOW VARIABLES LIKE bind_address;或者从另一台机器用nc测试端口连通性nc -zv 10.0.1.10 3306不通就说明监听地址生效了。另一个更严格的选项是skip-networking只允许本地socket连接但一般业务用不上除非是单机部署。实际环境中生产库建议用防火墙把3306端口限制到应用服务器网段在云环境下用安全组实现双保险才稳妥。接着说禁用root远程登录。MySQL的root账号默认是rootlocalhost但也有人为了省事创建了root%。这个账号一旦被暴力破解出来就是整个数据库的最高权限。处理方案很直接先查有没有SELECT User, Host FROM mysql.user WHERE Userroot;如果有root%要么删除要么改名成只能本机登录RENAME USER root% TO rootlocalhost;如果业务上确实需要从远程管理数据库也不要直接用root而是创建一个带全套管理员权限的专用账号并通过SSL连接CREATE USER dbadmin10.0.0.% IDENTIFIED BY 强密码; GRANT ALL PRIVILEGES ON *.* TO dbadmin10.0.0.% WITH GRANT OPTION;注意这里给了ALL也是有意的因为管理员账号必须能管理所有库但关键是账号名是明确的dbadmin连接来源限制在网段内密码用强口令后续所有审计日志都能追溯到具体账号。3.2 操作五开启SSL/TLS加密传输很多运维有个误区数据库在内网传输不用加密。实际上内网也不是绝对安全的网络抓包、交换机镜像、ARP欺骗都可能截获明文流量。MySQL客户端和服务器之间的通信如果不加密账号密码和业务数据都可能是裸奔的。MySQL 5.7及以上版本默认已经生成了自签名SSL证书可以用下面的命令确认SHOW VARIABLES LIKE have_ssl; SHOW VARIABLES LIKE ssl_ca;如果have_ssl为YES说明SSL能力已经开启。但只开能力不够还得让用户必须用SSL连接。默认情况下客户端可以自行选择是否加密要想强制执行ALTER USER appuser10.0.0.% REQUIRE SSL;这样即使是账号密码被截获攻击者没有证书也建立不了连接。如果自签名证书想换成企业CA签发的正式证书需要在my.cnf配置[mysqld] ssl-ca /data/mysql/ca.pem ssl-cert /data/mysql/server-cert.pem ssl-key /data/mysql/server-key.pem然后重启。客户端连接时指定证书参数mysql -h dbhost -u appuser -p --ssl-modeREQUIRED --ssl-ca/path/to/ca.pem这里有个细节要注意MySQL 8.0默认的caching_sha2_password插件在非SSL连接下首次认证会要求额外的RSA公钥交换容易导致一些老版本客户端连接报错。开了SSL之后这个机制就不再需要走公钥交换流程反而会减少这类连接问题。开启SSL后要定期巡检连接是否真的走了加密看连接线程的Ssl_cipher状态SHOW STATUS LIKE Ssl_cipher;如果连接没有加密这条线程的Ssl_cipher字段会是空的或者所有连接的汇总值会变化可以根据这个判断客户端是否都用了加密配置。3.3 操作六收紧配置文件与数据目录权限MySQL的配置文件和数据库文件放在系统上权限如果太开放任何一个能登录服务器的低权限用户都能把数据目录拷走。这个操作不需要进数据库直接操作系统层面。首先检查配置文件权限ls -l /etc/my.cnf /etc/mysql/my.cnf 2/dev/nullmy.cnf里可能包含临时密码、账号信息等敏感内容建议设置为600或640属主设为mysql用户或rootchmod 600 /etc/my.cnf chown root:mysql /etc/my.cnf其次检查数据目录权限ls -ld /var/lib/mysql数据目录必须属于mysql用户权限设置750或700。750的含义是mysql用户完全控制mysql组可读可执行其他用户没有任何权限。如果之前用root初始化过数据库可能出现数据目录属主不对的情况需要修正chown -R mysql:mysql /var/lib/mysql chmod 750 /var/lib/mysql这里有一个很多人踩过的坑直接对整个数据目录执行chown -R可能会把已经生成的binlog文件、慢查询日志的属主一并改了导致MySQL进程无法写入报错。建议先停库再改权限改完再启动如果不停库至少要把日志文件单独检查一遍。另外还要提防home目录下的.my.cnf。有些DBA图方便在~/.my.cnf里写了用户名密码这个文件权限如果是644任何能登录系统的用户都能看到数据库口令。检查并处理chmod 600 ~/.my.cnf顺便把历史命令里的明文密码清理一下。以前有个同事执行mysql -uroot -pRoot123456密码直接留在.bash_history里后来整个服务器被拖库这算一个经典教训了。3.4 操作七用独立低权限系统账号运行MySQLMySQL进程必须以专用系统账号运行绝不能用root启动mysqld。原因很简单MySQL在某些历史版本和特定场景下存在通过SQL注入或自定义函数提权执行系统命令的风险如果用root跑攻击者获得数据库权限后直接就是系统最高权限那整个服务器就沦陷了。目前主流发行版用包管理器安装的MySQL会自动创建mysql系统账号检查一下ps aux | grep mysqld看进程第一列的运行账号。如果不是mysql需要修正。用systemd管理的话检查服务单元systemctl cat mysqld在[Service]段应该能看到Usermysql没有的话要加上然后重启服务[Service] Usermysql Groupmysql如果是用mysqld_safe手工启动的要加--usermysql参数mysqld_safe --usermysql 还要确保mysql系统账号不能登录shellgrep mysql /etc/passwd正常应该是/sbin/nologin或/bin/false如果是/bin/bash执行usermod -s /sbin/nologin mysql这个操作不会影响mysqld进程运行因为mysqld启动后不会调用shell。但要注意如果后续要用crontab里带mysql命令的脚本脚本本身如果以mysql用户运行会受限需要在脚本里指定完整路径或者用sudo方式执行提前做好规划。4. 审计、备份与补丁管理让操作留痕让损失可恢复前面几条做的是预防后面这几条做的是“兜底”。安全加固做得再到位也不能假设永远不会被攻破关键是攻破之后能不能快速发现、能不能把损失降到最低。4.1 操作八开启审计日志让所有操作留痕一般情况下MySQL官方社区版不带审计插件企业版有MySQL Enterprise Audit但我们可以通过组合方案达到类似效果。最直接的是binlog只要开启了log_bin所有数据变更操作都会记录。检查是否开启SHOW VARIABLES LIKE log_bin;如果没开要在my.cnf里配置并重启[mysqld] log_bin /data/mysql/binlog/mysql-bin server_id 100开启binlog不只是为了数据恢复更重要的是它是追查数据被篡改的现场记录。binlog里能看到谁在什么时间执行了什么变更操作配合账号维度就能定位到人。只记录变更还不够对于SELECT等查询操作binlog不记录需要通用日志general_log来捕捉。生产环境不建议常开general_log它会产生大量日志拖垮性能。我的做法是在出现安全事件时临时打开比如追踪一段时间内所有SQLSET GLOBAL general_log ON; SET GLOBAL log_output TABLE;日志写到mysql.general_log表里排查完后立刻关闭并清理SET GLOBAL general_log OFF; TRUNCATE TABLE mysql.general_log;如果你用的是Percona分支或者有条件装审计插件建议直接上audit_log插件它能按用户、按匹配规则记录连接事件和SQL语句性能开销比general_log小得多还支持JSON格式输出配合日志采集系统做实时告警很合适。4.2 操作九备份加密与binlog保护备份是数据安全的最后一道防线但很多人做完备份就把备份文件晾在那儿既不加密也不限制访问权限。结果就是攻击者不用费劲破数据库直接把备份文件拖走即可。先看备份账号mysqldump用到的备份账号应该只给最小权限CREATE USER backup_userlocalhost IDENTIFIED BY 备份专用密码; GRANT SELECT, RELOAD, PROCESS, REPLICATION CLIENT, SHOW VIEW, EVENT, TRIGGER ON *.* TO backup_userlocalhost;这些权限足够执行逻辑备份但不足以修改业务数据或DROP任何表。备份命令配合加密传输mysqldump --single-transaction --set-gtid-purgedOFF \ -u backup_user -p \ --all-databases | gzip | openssl enc -aes-256-cbc -salt -pbkdf2 -k 加密密钥 backup_$(date %F).sql.gz.enc恢复的时候反过来解密再导入openssl enc -d -aes-256-cbc -pbkdf2 -k 加密密钥 -in backup_20240601.sql.gz.enc | gunzip | mysql -u root -p加密密钥不要写在脚本明文里建议放在只有root可读的密钥文件里或者使用密钥管理服务。备份文件的总大小如果过大也可以考虑物理备份工具比如开源生态中常见的Percona XtraBackup它支持流式备份配合加密备份速度比mysqldump快很多。binlog本身也是敏感文件里面记录了大量数据的变更过程binlog文件的权限也要限制chmod 660 /data/mysql/binlog/*同时设置合理的过期时间防止binlog无限堆积占用磁盘SET PERSIST binlog_expire_logs_seconds 604800;这个值表示binlog保留7天具体时长根据全量备份频率调整至少保留一个完整恢复周期。4.3 操作十补丁升级把已知漏洞堵住这个操作最不起眼但最关键。MySQL每隔一段时间会发布安全更新修复被公开的漏洞。很多时候攻击者就是拿公开漏洞打过期的数据库越老的版本越容易被一击命中。先确认当前版本SELECT VERSION();再对比官方发布的安全更新公告看看自己的版本是否在受影响范围内。如果版本过旧要制定升级计划。升级这事不能拍脑袋在生产库上直接搞正确流程是先在测试环境还原同样的版本、同样的数据验证应用兼容性再做生产升级。升级路径上要注意5.7升级到8.0需要关注字符集、认证插件、SQL兼容性差异建议先跑官方升级检查工具小版本升级比如8.0.33到8.0.36一般直接替换二进制包重启即可但也要看release notes里有没有行为变更每次升级前先备份出问题能回滚。如果暂时没法升级至少要确认MySQL部署在防火墙后面公网无法直接访问同时禁用不用的存储引擎和组件。比如某些版本中存在风险的组件可以通过配置文件禁用来降低攻击面[mysqld] disabled_storage_engines federated,blackhole禁用不用的引擎后即使存在针对特定引擎的漏洞攻击者也用不上。平时建议订阅官方的安全更新邮件漏洞公告出来第一周内评估一个月内完成补丁。提醒甲骨文官方会为每个版本提供补丁更新和已知问题的说明但我在这里不具体展开版本号不同分支和发行版的补丁节奏差异很大大家以自己实际部署的版本和发行渠道为准。5. 常见问题排查与加固效果验证十项操作全部梳理完了但执行过程中你大概率会遇到一堆问题。我把最常踩的几个坑整理成速查表并给出一套验证清单。5.1 加固后业务连接失败的典型原因最常出现的情况是收缩网络暴露面后应用服务器连不上数据库。排查链路是从应用服务器ping数据库IP再telnet 3306端口确认不通后检查MySQL监听地址和防火墙查看监听地址是否正确SHOW VARIABLES LIKE bind_address;查看防火墙规则是否正确放行云上查安全组物理机查iptables/firewalld确认不是本机socket连接被误杀应用如果走的是本机socket修改bind_address不影响第二个高频问题是强制SSL之后客户端连不上。旧版本的MySQL客户端或者某些开发语言的驱动默认不支持SSL连接时加入指定参数即可或者升级客户端版本实在兼容不了就不要给该账号强制REQUIRE SSL改成部分允许并做好网络隔离。第三个是权限回收后应用报权限不足。这种情况多半是回收权限时把应用账号在其他Host下的记录遗漏了应用实际用的Host和你REVOKE时写的Host不一致导致权限没变化或者变化不彻底用SHOW GRANTS多核验几遍。5.2 加固效果快速验证清单每次做完加固我习惯按下面这个清单走一遍几分钟就能判断整库基线是否达标检查项执行命令或方法达标标准匿名账号SELECT User,Host FROM mysql.user WHERE User;查出0条记录弱口令通过validate_password状态确认策略为STRONGpolicy2length12远程rootSELECT User,Host FROM mysql.user WHERE Userroot AND Hostlocalhost;查出0条记录监听地址SHOW VARIABLES LIKE bind_address;非0.0.0.0SSL能力SHOW VARIABLES LIKE have_ssl;值为YES配置权限ls -l /etc/my.cnf600或640运行账号ps auxgrep mysqldbinlog状态SHOW VARIABLES LIKE log_bin;值为ON密码有效期SHOW VARIABLES LIKE default_password_lifetime;建议90天左右版本状态SELECT VERSION();跟官方最新安全更新做对比这张表我建议存下来做成一个安全巡检脚本的检查项每个月自动跑一遍输出结果留档。巡检比一次加固更重要因为配置是会漂移的今天改的bind_address明天可能被某个安装包重置不持续盯就会回到裸奔状态。5.3 一套轻量巡检脚本思路这里分享一个纯shell加SQL组合的巡检思路不用装额外agent。核心就是把上面表格里的SQL和系统命令包在一个脚本里结果输出到文件#!/bin/bash MYSQL_CMDmysql -uroot -p${MYSQL_PWD} -e echo $(date) MySQL Security Check /var/log/mysql_security_check.log $MYSQL_CMD SELECT User,Host FROM mysql.user WHERE User; /var/log/mysql_security_check.log $MYSQL_CMD SHOW VARIABLES LIKE validate_password%; /var/log/mysql_security_check.log $MYSQL_CMD SELECT User,Host FROM mysql.user WHERE Userroot AND Hostlocalhost; /var/log/mysql_security_check.log $MYSQL_CMD SHOW VARIABLES LIKE bind_address; /var/log/mysql_security_check.log $MYSQL_CMD SHOW VARIABLES LIKE have_ssl; /var/log/mysql_security_check.log $MYSQL_CMD SHOW VARIABLES LIKE log_bin; /var/log/mysql_security_check.log ls -l /etc/my.cnf /var/log/mysql_security_check.log ps aux | grep mysqld | grep -v grep /var/log/mysql_security_check.log配合crontab每周执行一次再写个简单的日志告警规则发现非预期账号、弱策略、裸奔监听时发通知。这一套做下来基本可以覆盖日常90%的安全基线需求。其实回看这十项操作真正“硬核”的地方不在于用了什么复杂工具而在于把基础动作做到位、做持久。我自己处理过好几个数据库安全事件最后复盘时发现只要当初认认真真做了权限最小化和端口收敛后面那些复杂的攻防根本轮不到上场。每次接手新库我都会先花15分钟跑一遍验证清单确认没有高危项再谈后续优化。这套习惯坚持了几年虽然不敢说库库铁桶但至少再没在凌晨被删除告警叫醒过。你也可以把这十条当作起点慢慢补齐自己环境里特有的检查项比如非root系统账号之外是否还有sudo提权风险、监控平台是否能看到关键指标的异常波动、数据脱敏是否覆盖了非生产环境这些都是下一个层次的事了。先把基础打牢比什么都强。