我接手过不少 MySQL 实例第一件事永远不是看性能而是先翻错误日志里 Access denied 出现的频率。有一次半夜两点日志里每分钟刷几十条Access denied for user root... using password: YES3306 端口又被人拿字典撞库了。这类攻击不算高明但特别烦人IP 可以换、用户名可以换你封一个它换一个。MySQL 其实自带一个专门治这个问题的插件叫 Connection Control连接控制插件干的事情很纯粹当某个客户端连续输错密码达到阈值后服务器开始对它的连接请求做递增延迟让暴力破解在时间上耗不起。这篇文章就从安装、调参到验证效果完整过一遍适合手里有 MySQL 5.7 或 8.0 实例、想让数据库自己扛住一部分撞库压力的同学也适合刚接触数据库安全、想给团队加一道保险的新手。1. 为什么数据库层面需要防暴力破解1.1 攻击者是怎么打你的 3306 端口的暴力破解的核心逻辑其实很土拿一份弱口令字典然后用工具批量尝试用户名和密码组合。MySQL 的安全问题里弱口令被爆破的占比一直不低。很多团队觉得“我有云安全组”“我在内网”“端口没对外开放”但实际上总有漏网的时候某个测试环境把 3306 映射出去了、某个临时加白名单忘了关、或者是内网里一台机器被攻破后攻击者横向移动时扫内网网段找 3306 继续撞库。数据库层面的弱口令爆破和网站后台爆破一样是内网渗透标准流程里必不可少的一步。一旦账号被撞出来攻击者拿到的不是一台服务器而是你整个数据集的读取和删除权限。轻则数据被拖走重则直接被删库勒索。所以防爆破不该被当成安全团队的事DBA 自己就该把它当成基本功。而真正落到生产环境你需要的不是“知道有人在打你”而是 MySQL 自己能先撑住一阵子把攻击节奏拖慢给你留出封 IP、改密码的时间。1.2 MySQL 自带的安全网为什么不够用MySQL 默认有两个东西和防爆破沾边但都不够。第一个是错误日志里反复出现的Access denied for user它只负责记录不负责拦截。第二个是max_connect_errors参数默认 100当某个客户端“不能正常完成握手”的次数超过这个值服务器会直接拒绝该主机的后续连接。问题在于它统计的是 TCP 握手和连接建立阶段就出问题的连接而密码错误其实已经走完了握手、进入了认证阶段根本不怎么计入这个计数里。换句话说攻击者只要老老实实建立连接、提交错误密码max_connect_errors大概率一直不触发。MySQL 8.0.19 之后CREATE USER和ALTER USER支持了FAILED_LOGIN_ATTEMPTS和PASSWORD_LOCK_TIME可以让账号连续失败 N 次后直接锁定。这个机制很有用但它是“硬锁”存在一个双刃剑问题攻击者如果故意对一个高权限账号乱输密码可以把正常账号也锁掉等于变相制造拒绝服务。所以生产环境里纯靠“锁死”并不能解决所有场景。1.3 Connection Control 的设计思路Connection Control 的思路是“延迟而不是锁死”。它不会让账号失效而是让服务器对失败连接请求的响应越来越慢。对正常的同事来说偶尔手滑输错一两次密码几乎无感知但对一个每秒尝试几十次密码的爆破工具来说延迟是致命的每一轮尝试都要多等好几秒整个字典跑完的时间成本会从几分钟变成几天。它还有一个很有价值的特性计数是分账号维度的具体是userhost维度。也就是说某个账号连续失败才会被延迟不同账号之间的计数不串扰。再加上它支持最大延迟上限可以避免延迟无限拉长导致自己人都连不上。当然它也有边界对于从海量 IP 轮流来试同一个账号的分布式攻击单账号计数会被分散插件容易误以为“没有攻击”。所以它是纵深防御里的第一道防线不是唯一防线这一点后面会展开说。2. 安装与启用两分钟让插件跑起来2.1 先搞清楚两个插件的关系Connection Control 实际是两个插件名字很容易混淆CONNECTION_CONTROL和CONNECTION_CONTROL_ADMIN。前者负责真正干活的部分——监听连续失败次数、计算并执行延迟后者负责“管理面”——提供connection_control_*系统变量和INFORMATION_SCHEMA.CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS视图。如果只装CONNECTION_CONTROL你连SHOW VARIABLES LIKE connection_control%都查不到参数所以官方文档里的标准操作也是两个一起装。2.2 在线安装与验证安装命令很简单在 MySQL 客户端里直接执行INSTALL PLUGIN CONNECTION_CONTROL SONAME connection_control.so; INSTALL PLUGIN CONNECTION_CONTROL_ADMIN SONAME connection_control.so;装完验证一下SHOW PLUGINS; -- 或者更精确地查 SELECT PLUGIN_NAME, PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS WHERE PLUGIN_NAME LIKE connection%;两个插件状态都是 ACTIVE 就说明加载成功。需要说明的是connection_control.so这个文件是 MySQL 安装包自带的官方二进制 tar.gz、RPM 包、Docker 官方镜像里都有。我分别在 5.7.44 和 8.0.36 的社区版上试过都能正常加载。如果INSTALL PLUGIN报ERROR 1126 (HY000): Cant open shared library connection_control.so先执行SHOW VARIABLES LIKE plugin_dir;查看插件目录再去系统里 ls 一下这个文件在不在不在就说明装的是精简版或者路径不对重新装完整的 mysql-server 包即可。2.3 让配置重启后依然生效INSTALL PLUGIN本身会把插件注册到mysql.plugin系统表重启后会自动加载。所以在最简场景下你不需要额外写配置。但生产环境一般还会把参数直接写进配置文件方便统一管理和自动化部署[mysqld] plugin-load-addconnection_control.so loose-connection-control-failed-connections-threshold5 loose-connection-control-min-connection-delay1000 loose-connection-control-max-connection-delay60000 loose-connection-control-failed-connections-limit0这里有两个细节容易踩坑。第一loose-前缀很关键它的意思是“这个变量如果存在就加载不存在也别报错”。万一哪天插件没加载成功MySQL 不会因为找不到一个不认识的变量而拒绝启动。第二plugin-load-add和plugin-load的区别在于前者是追加加载后者如果多处配置容易互相覆盖。如果你已经在配置里写plugin-load-add就不需要再用INSTALL PLUGIN二选一即可如果之前已经INSTALL PLUGIN过了再写plugin-load-add也不会怎么样但没必要重复。运行时可以直接用SET GLOBAL改参数立即生效不用重启SET GLOBAL connection_control_failed_connections_threshold 5; SET GLOBAL connection_control_min_connection_delay 1000;但SET GLOBAL改的东西重启后会丢想持久化还是要写配置文件。用 Docker 跑 MySQL 的话把配置片段放到挂载进容器的/etc/mysql/conf.d/目录下重启容器即可。2.4 卸载也别搞错顺序卸载时顺序和安装相反先卸管理员插件再卸核心插件UNINSTALL PLUGIN CONNECTION_CONTROL_ADMIN; UNINSTALL PLUGIN CONNECTION_CONTROL;同时把my.cnf里对应的plugin-load-add和loose-开头的参数删掉不然重启后又会自动装回来。这个顺序我踩过坑反过来先卸核心插件管理员插件会残留或者报依赖错误老老实实按顺序走一遍最省心。3. 核心参数与延迟计算原理3.1 四个系统变量对照表Connection Control 一共四个系统变量版本不同数量不同。5.7 里只有前三个8.0.13 之后多了第四个整理成表格看得更清楚系统变量默认值作用connection_control_failed_connections_threshold3连续失败多少次后开始延迟设为 0 表示关闭延迟功能connection_control_min_connection_delay1000最小延迟毫秒数同时也是每次失败的递增步长connection_control_max_connection_delay2147483647最大延迟毫秒数默认值约等于 24.8 天相当于不封顶生产建议调小connection_control_failed_connections_limit08.0.13 新增连续失败达到该值后封禁账号0 表示不封禁默认阈值是 3。这个默认值的意思是“连续错三次就开始算账”。正常人确实很少连着输错三次密码但应用连接池如果配置错误可能一秒内就连续失败很多次所以很多生产环境会把阈值调到 5 甚至更高避免误伤。min_connection_delay默认 1000 毫秒也就是从 1 秒起步。3.2 延迟计算公式为什么越错越慢延迟不是固定的而是线性递增的公式可以写成这样D 0 当 N T D MIN((N - T) * S, M) 当 N T N 该账号连续失败次数 T connection_control_failed_connections_threshold S connection_control_min_connection_delay M connection_control_max_connection_delay拿 T3、S1000ms、M60000ms 举例实际的延迟表现是这样的连续失败次数是否延迟本次失败要被拖多久第 1~3 次否0第 4 次是1 秒第 5 次是2 秒第 6 次是3 秒第 7 次是4 秒...是继续递增第 63 次及以上是封顶 60 秒这个线性增长的效果非常可观。如果 T5、S1000ms、M60000ms理论上一个攻击者对同一账号跑 10 万次字典前 5 次没有延迟第 6 次开始每次至少等 1 秒到第 65 次以后每次都要等满 60 秒光时间成本就从几分钟变成几十天绝大多数批量爆破工具根本跑不完。但我想强调一点这条防线防的是“少数账号被高频尝试”的情况。如果攻击者同时用几万个账号各试一次密码每个账号的失败次数都到不了阈值插件就相当于没启动。这种情况要靠网络层防护去兜底后面第 5 节会说怎么配合。3.3 计数在哪里看、什么时候清零插件的失败计数可以在系统视图里查到SELECT * FROM INFORMATION_SCHEMA.CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS\G这个视图只展示失败计数大于 0 的账号字段就两个USERHOST和FAILED_ATTEMPTS。注意USERHOST是一个带引号的完整字符串显示格式像rootlocalhost查询时直接SELECT *就行不要想着拆字段去比对容易出错。计数清零的时机有两个该账号成功登录一次或者整个 MySQL 实例重启。这个特性很重要意味着如果你的测试账号把自己刷进了延迟用正确密码登录一次就能恢复正常。另外还有一个状态变量值得盯SHOW GLOBAL STATUS LIKE Connection_control_delay_generated;这个变量统计服务器一共生成过多少次延迟它是一个只增不减的计数器。如果它突然快速增长基本可以断定当前有程序在批量尝试失败登录这时候就该去查错误日志和网络来源了。4. 实战模拟一次撞库眼见为实4.1 准备一个测试账号为了让效果可复现我建议在本地起一个测试账号不要直接在公网实例上玩。执行下面几条 SQLCREATE USER brutetest127.0.0.1 IDENTIFIED BY Pass_111;这个账号只需要能走通认证就行不需要任何业务权限。注意127.0.0.1和localhost在 MySQL 里是两个不同的 host后面测试脚本里要固定用127.0.0.1去连不然计数会对不上。4.2 用脚本连续输错密码先设一个比较敏感的配置方便观察SET GLOBAL connection_control_failed_connections_threshold 3; SET GLOBAL connection_control_min_connection_delay 1000; SET GLOBAL connection_control_max_connection_delay 60000; SET GLOBAL connection_control_failed_connections_limit 0;然后用 shell 循环连续输错密码并且记录每次的耗时for i in $(seq 1 8); do s$(date %s%N) mysql -h127.0.0.1 -ubrutetest -pWrongPass --connect-timeout3 -e SELECT 1 /dev/null 21 e$(date %s%N) echo 第 ${i} 次失败耗时 $(( (e - s) / 1000000 )) ms done如果机器上装了 pymysql也可以用 Python 脚本输出更直观import time import pymysql for i in range(1, 9): t0 time.time() try: pymysql.connect( host127.0.0.1, port3306, userbrutetest, passwordWrongPass, connect_timeout3, ) except pymysql.err.OperationalError as e: print(f第 {i} 次失败 用时 {time.time() - t0:.2f}s {e}) else: print(f第 {i} 次竟然成功了) time.sleep(0.3)正常情况下你会看到前三次失败基本秒回第四次开始出现 1 秒左右的耗时第五次 2 秒第六次 3 秒。这就是插件在生效每一次失败的响应都在变慢攻击者拿不到结果就不知道该继续还是换密码。4.3 从失败计数表和状态变量验证跑完脚本后查一下计数SELECT * FROM INFORMATION_SCHEMA.CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS\G输出大概是*************************** 1. row *************************** USERHOST: brutetest127.0.0.1 FAILED_ATTEMPTS: 8这里记录的 8 就是刚才连续失败的次数。此时用一个正确密码登录一次mysql -h127.0.0.1 -ubrutetest -pPass_111 -e SELECT 1再重新查这张表能看到这一行消失了因为成功登录清零了计数。这个特性在实际运维中很有用测试环境误伤了自己人成功登录一次就能解除延迟状态不需要重启数据库。4.4 更进一步8.0 的封禁模式和账号锁双保险如果你的实例是 8.0.13 以上可以开启封禁模式让失败次数达到上限后直接拒绝对应账号的后续连接SET GLOBAL connection_control_failed_connections_limit 8;开启后继续用错误密码测试你会发现在连续失败 8 次之后第 9 次连接不再走“延迟然后拒绝”的流程而是在当前延迟窗口内直接被拒绝。测试完记得把参数调回 0避免封住了自己的运维账号。8.0.19 以上还可以用账号级别的硬锁策略ALTER USER brutetest127.0.0.1 FAILED_LOGIN_ATTEMPTS 3 PASSWORD_LOCK_TIME 2;这个配置的意思是该账号连续失败 3 次后锁定 2 天期间就算密码输对了也进不来。如果测试时把自己锁了用ALTER USER brutetest127.0.0.1 ACCOUNT UNLOCK;可以主动解锁。这两套机制不冲突Connection Control 负责拖慢节奏账号锁负责在合适时机一刀切断。生产上我一般让 Connection Control 先扛账号锁只给高权限账号开。5. 常见问题与排查实录5.1 插件装了却看不到任何 connection_control 参数这是最高频的问题。大部分情况是只装了CONNECTION_CONTROL没装CONNECTION_CONTROL_ADMIN。系统变量和视图都是由 ADMIN 插件提供的核心插件只管监测和延迟。解决办法很简单补上第二条INSTALL PLUGIN命令再SHOW VARIABLES LIKE connection_control%就能看到了。5.2 INSTALL PLUGIN 报 1126找不到共享库ERROR 1126 (HY000): Cant open shared library connection_control.so。先检查插件目录SHOW VARIABLES LIKE plugin_dir;然后在系统里看文件在不在ls -l /usr/lib64/mysql/plugin/connection_control.so文件不存在就要检查是不是装的 MySQL 客户端而不是服务端或者安装时裁剪了插件目录。在 RHEL/CentOS 上一般yum install mysql-server完整安装后就会带。如果文件存在但还是报错可能是 mysql 进程用户没有读权限或者目录被挂载成 noexec 导致无法加载逐个排查这两点基本能解决。5.3 延迟没有按预期出现不是所有失败都会被计数。插件主要统计进入认证阶段后返回Access denied的失败客户端在 TCP 建立阶段就断开、握手没完成、连接被防火墙直接 RST 这类情况通常不会计入。另外计数是按userhost分开的测试时如果一会儿用127.0.0.1、一会儿用localhost去连会被当成两个账号分别计数。还有一点经常被忽略实例重启之后计数全部清零如果你重启过就理解为什么延迟消失了。最后再确认下min_connection_delay单位是毫秒别把 1000 想当然写成 1。5.4 会不会误伤正常用户和应用连接池会而且很容易。之前我讲过默认阈值是 3一个同事手滑输错三次第四次就会被拖 1 秒应用连接池如果配置的密码错误启动时它会高频重试这时候整个池子都会陷入“连不上、等待、再连”的循环应用启动时间暴涨。排查办法是看这两个值SHOW GLOBAL STATUS WHERE VARIABLE_NAME IN (Aborted_connects, Connection_control_delay_generated);如果Aborted_connects和delay_generated双高说明确实有大量失败认证。应急处理可以先把阈值临时改大甚至关闭延迟SET GLOBAL connection_control_failed_connections_threshold 100;等应用修复密码后再调回正常值。生产环境的建议是应用账号和管理员账号分开策略应用账号阈值设高一点管理员账号另外通过内网和防火墙做访问控制。5.5 主从复制账号会影响吗正常情况下不会。只要复制账号密码正确主从复制不涉及失败认证插件对复制链路毫无感知。但如果你改了复制账号密码忘了同步到从库或者复制账号被误删IO 线程反复连接失败它同样会被计数并延迟表现就是错误日志里一直刷Access denied复制延迟持续拉大且从库重连间隔越来越长。处理顺序是先改对密码、恢复复制再考虑是否需要重启实例清掉计数。5.6 常见问题速查表现象可能原因处理办法查不到 connection_control 参数只装了核心插件没装 ADMIN补INSTALL PLUGIN CONNECTION_CONTROL_ADMININSTALL 报 1126插件文件不存在/权限问题检查 plugin_dir文件缺失则补装完整 mysql-server延迟没生效阈值设为 0 / host 不一致 / 重启过确认参数统一用相同主机名连接应用连接池启动超时密码配置错误触发延迟修密码临时调高阈值主从延迟增大且报 Access denied复制账号密码错误改密码后重启实例清计数多 IP 轮番尝试不触发单账号单 IP 计数分散配合 fail2ban 或云安全组封来源想手动清空计数无直接清理命令该账号成功登录一次或重启实例6. 我的调优建议与一点体会根据遇到的场景我给一个可以直接抄的基线配置。内网有堡垒机、访问走固定跳板的环境threshold3、min1000、max30000是比较舒服的组合既能拦住爆破又基本不影响正常使用。3306 有一定暴露面、测试环境账号乱的环境建议threshold5、min1000、max60000必要时把failed_connections_limit设成 8 到 10但要保证运维账号是独立账号别跟应用共用。日常监控也别只看业务指标把这两个状态变量加进告警里SHOW GLOBAL STATUS WHERE VARIABLE_NAME IN (Aborted_connects, Connection_control_delay_generated);一旦Connection_control_delay_generated在短时间内快速增长基本就是有人在批量试密码。这时候配合错误日志里的Access denied for user来源 IP去防火墙或安全组封掉来源才是一个完整的处置闭环。最后说点个人体会。我早期把阈值设成 2、最小延迟设成 5000ms觉得越敏感越好结果开发同事连着输错两次把测试环境的应用账号拖进了延迟窗口一早上全是连接超时告警排查了大半天才意识到是插件在起作用。后来我把规矩定成阈值不低于 5最大延迟一定要设置所有生产改动都要先检查是否会误伤“自己人”。安全策略最重要的是别把自己锁死Connection Control 的“延迟而不是锁死”本身就是这个思路用它的时候也应该用它自己的逻辑来约束我们的配置习惯。