暴力破解MySQL密码这件事,很多团队一开始都不当回事,直到某天发现数据库端口被扫烂、错误日志堆了几万条Access denied,甚至业务账号真的被撞库撞穿,才急急忙忙来找解决方案。如果你也是这种状态,或者你想在问题发生之前先把防线补上,MySQL官方自带的Connection Control插件就是目前性价比最高、也最省事的一个选择。这篇文章就把它的原理、安装、调参、踩坑和周边协同一次讲透。
1. 暴力破解思路与Connection Control的设计初衷
1.1 为什么防火墙和WAF挡不住针对MySQL的暴力破解
很多人的第一反应是:我服务器有安全组,有防火墙,有WAF,怎么还会被暴力破解?这里有个认知误区。安全组和防火墙确实能挡住绝大部分来自外部的扫描,但MySQL的3306端口一旦对公网开放,或者内网有被攻陷的跳板机,攻击者的请求从源头上就是合法连接。WAF更多防护的是HTTP层的应用攻击,对数据库这种协议层的连接请求,它往往只能看个IP,没法深入判断"同一个账号连续试密码"是不是攻击行为。
也就是说,暴力破解在最底层其实是一个身份认证失败的过程。攻击者不关心你的业务逻辑,他只关心连接、试密码、断开、再连接,循环往复。这种请求和正常业务用户偶尔输错密码在形态上高度相似,传统网络层设备根本没法区分。所以,防暴力破解必须落到数据库引擎自身,在认证这一层做拦截,这正是Connection Control存在的理由。
1.2 Connection Control做了两件事
Connection Control是MySQL 5.7.17版本开始内置的一个插件族,它分两部分:
- connection_control:主插件,负责记录每个账号的连续失败尝试次数,并触发延迟惩罚。
- connection_control_failed_login_attempts:辅助插件,把失败信息写入
information_schema和performance_schema的对应表中,方便DBA观察和分析。
它的工作逻辑非常朴素:正常用户输错密码,输个三次五次就该停手了。如果一个账号在两分钟内连续失败20次,那就不是手误,而是有自动化脚本在跑。插件一旦判定"这像暴力破解",就会让下一次连接请求在数据库层强制等待一段时间,等待时间会随着连续失败的次数递增。
这个思路说白了就是给暴力破解增加时间成本。脚本跑一轮要等几秒甚至几分钟,破解速度瞬间从每秒几十次降到几十次每小时,攻击者自己就会失去耐心。而正常用户的体验几乎无感,因为阈值没触发之前,系统不做任何额外处理。
1.3 核心参数只认三个
Connection Control虽然拆成两个插件模块,但真正需要DBA手动调的参数只有三个:
- connection_control_failed_connections_threshold:失败多少次后开始触发延迟,默认值是3,即连续失败3次,第4次开始被惩罚。这个值可以设为0,设为0表示不开启惩罚功能。
- connection_control_min_connection_delay:单次最小延迟毫秒数,默认1000毫秒,也就是1秒。第一次触发惩罚时至少等这么久。
- connection_control_max_connection_delay:单次最大延迟毫秒数,默认2147483647毫秒,约等于24.8天。设置上限是防止延迟无限扩大把账号彻底焊死。
我个人的理解是,这三个参数组合起来就是一个"越挫越勇的保安":刚开始只是让你进门等10秒,如果你还继续在门口试密码,下次就等20秒、40秒,直到你受不了走人。
2. 从零到一:安装、启用与参数计算
2.1 安装插件,注意别漏了第二个
MySQL 8.0和5.7.17以上的版本中,插件文件已经随发行版自带,不需要额外下载。安装有两种方式。
第一种,直接使用SQL语句动态安装:
INSTALL PLUGIN connection_control SONAME 'connection_control.so'; INSTALL PLUGIN connection_control_failed_login_attempts SONAME 'connection_control_failed_login_attempts.so';注意,我见过很多教程只装第一个,结果后来查询失败记录时发现information_schema.CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS表不存在,才跑回来补装第二个。这两个插件建议始终成对安装,因为主插件负责拦截,辅助插件负责给你提供"证据"。
安装后可以确认一下:
SELECT PLUGIN_NAME, PLUGIN_STATUS, PLUGIN_TYPE FROM information_schema.PLUGINS WHERE PLUGIN_NAME LIKE 'connection%';第二种方式,在my.cnf配置文件中写死,让MySQL启动时就加载:
[mysqld] plugin-load-add = connection_control.so plugin-load-add = connection_control_failed_login_attempts.so用配置文件方式的好处是实例重启后插件依然自动加载,避免手动安装后机器重启、插件状态丢失的坑。坏处是修改配置需要重启MySQL,所以我的习惯是:测试环境用INSTALL PLUGIN,生产环境直接改配置。
2.2 参数计算:从目标延迟倒推阈值
很多人直接把三个默认参数装上就不管了,这其实是偷懒。默认值3次失败即触发、最小延迟1秒,对面向公网的实例来说这个灵敏度偏高,会误伤偶尔输错密码的正常用户;对纯内网的核心库来说,3次又太松,攻击者可以快速试几百个密码。所以参数设置要结合业务场景计算。
先说公式。假设你设置的失败阈值为N,最小延迟为T_min,那么:
- 第N次失败后,下一次连接(也就是第N+1次)开始被延迟,延迟时间至少为T_min。
- 如果继续失败,延迟时间会逐渐递增,递增的规律是每次增加T_min,直到达到最大延迟T_max。
实际观测中,MySQL官方文档并没有规定严格的递增公式,社区普遍观察到的行为是:延迟从T_min开始,每多一次失败,延迟增加约T_min,直到封顶T_max。举个例子,阈值10、最小延迟1秒,那么第11次连接等待约1秒,第12次约2秒,第13次约3秒……直到达到最大延迟上限。
如果你希望攻击者在触发惩罚后,平均每轮只能尝试约6次密码(即平均每次延迟约10秒),那么可以这样推算:让第N+1次连接的延迟达到10秒附近,即T_min=10秒,而这是第一次触发的等待时间,之后每隔10次左右就翻倍。实际生产环境中不太需要精确计算到单次,更实用的做法是:
明确业务诉求:你能接受业务账号输错几次密码后被锁?
我把这个诉求翻译成参数:
| 业务场景 | 失败阈值 | 最小延迟(秒) | 最大延迟(秒) | 设计意图 |
|---|---|---|---|---|
| 纯内网、低频业务系统 | 5 | 5 | 600 | 误伤率低,内网攻击源有限,惩罚足够 |
| 面向公网、有APP用户直接连库 | 10 | 1 | 3600 | 留足手误空间,惩罚缓慢拉长 |
| 高安全等级、核心交易库 | 3 | 10 | 86400 | 快速惩罚,威胁者基本被劝退 |
2.3 在线调整参数并持久化
在MySQL 8.0中,这三个参数都是动态变量,不需要重启就能改。但在线改完之后要注意,动态改的参数重启后会恢复默认值。如果你希望永久生效,需要把参数写入my.cnf。
在线修改命令如下:
SET GLOBAL connection_control_failed_connections_threshold = 8; SET GLOBAL connection_control_min_connection_delay = 2000; SET GLOBAL connection_control_max_connection_delay = 3600000;确认修改生效:
SHOW VARIABLES LIKE 'connection_control%';然后到my.cnf中同步写入对应的配置段,保证重启后参数不会回退。
一个要注意的坑:这三个参数如果你在命令行只改了GLOBAL级别,当前已有的连接不会受影响,新建立的连接才会按照新规则执行。对于高并发的业务,这种"渐变生效"其实反而是优点,不会出现瞬间把所有连接掐断的情况。当然如果你希望立即生效,可以再执行SET PERSIST(MySQL 8.0+支持),让它同时写入mysqld-auto.cnf:
SET PERSIST connection_control_failed_connections_threshold = 8;3. 从参数到实战:连接失败延迟的底层机制
3.1 失败次数到底怎么计数的
我们实际压测的时候发现一个很有意思的细节:Connection Control的失败计数是按账号+客户端主机两个维度组合来统计的,不是单纯按用户名来算。什么意思?同一个业务账号app_user,从A机器连续输错密码,和从B机器连续输错密码,这两个会被视为两条独立记录。这一点在information_schema.CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS表里体现得非常明显,每一行都有一个USERHOST字段,存的是类似app_user@192.168.1.10这样的组合。
这个设计其实很聪明。攻击者拿到了一堆弱口令账号,他用同一台肉鸡来试,那么同一USERHOST下的失败次数会快速累加。而正常用户如果从家里和公司两个IP轮流登,即便都输错过密码,影响也被隔离了,不会因为总失败次数超标而被误伤。
3.2 延迟产生的过程还原
为了把机制讲透,我实际搭了一个测试环境模拟。假设我设置了阈值3、最小延迟5秒、最大延迟60秒。现在模拟连续失败:
- 第1次失败:不延迟,正常返回Access denied。
- 第2次失败:不延迟,正常返回Access denied。
- 第3次失败:不延迟,正常返回Access denied。
- 第4次失败:连接请求进入等待队列,5秒后才返回Access denied。
- 第5次失败:继续递增,约10秒后返回Access denied。
- 第6次失败:约15秒后返回Access denied。
到了第7次、第8次,连接等待时间已经接近30秒,此时客户端大概率已经超时。从攻击者的视角来看,脚本的效率急剧下降,跑了十几分钟可能也试不出几十个密码。从正常用户视角来看,输错三次密码后,再试时要等5秒,这个体验虽然有点"卡",但对于防止账号被盗来说是完全可以接受的。
3.3 成功登录后计数会重置
有一个问题很多人没搞明白:如果我输错了两次密码,第三次输对了,那这个"失败2次"的记录会保留多久?会不会下次我再输错一次就直接触发延迟?
答案是:不会。一旦该账号在该USERHOST上成功登录,失败计数器立即清零。这是Connection Control非常人性化的设计。它惩罚的不是"曾经输错过密码的人",而是"一直在输错密码的人"。所以正常用户完全不用担心自己某天手滑多按了几次错误密码,就被这个插件盯上好几个小时。
当然,如果你在同一个USERHOST上连续失败到触发延迟,中途放弃了,没有成功登录,那么计数器会继续保留。保留多久取决于MySQL实例的运行时长和计数刷新策略,在实践中可以理解为持续累积,直到出现一次成功登录或者达到某个内部清理条件。
4. 生产环境高频问题与排查实录
4.1 插件装上了,却不生效
这是问得最多的问题。很多DBA装完插件后,测试了一下连续输错密码,发现数据库还是秒回Access denied,一点延迟都没有。查了一圈,发现是参数connection_control_failed_connections_threshold被设成了0。
MySQL官方文档里写明:该参数设置为0时,表示关闭惩罚功能。装完插件后,默认值虽然是3,但如果你的实例之前设置过--connection-control-failed-connections-threshold=0,或者你在my.cnf里写死过这个值,插件即使加载了也不会产生实际效果。
排查方法很简单:
SHOW VARIABLES LIKE 'connection_control_failed_connections_threshold';如果看到0,改回非0值就行。这种事靠看日志看不出来,因为插件根本没报错,它只是不干活。
4.2 参数改了,延迟时间还是不对
有同行跟我反馈,说设置了最小延迟为10秒,但实际观察下来,第4次连接只等了2秒,百思不得其解。
这里要再强调一次:最小延迟是"下限",不是"固定值"。当失败次数超过阈值后,延迟从最小值开始递增。也就是说,设置min_connection_delay=10000,只代表第一次触发惩罚时的等待时间至少为10秒,但如果你的连续失败刚好卡在阈值刚被突破的位置,MySQL内部会结合其他因素给出一个不小于10秒的等待值。实践中这个值会非常接近10秒,但不会少于它。如果你观察到的延迟时间远小于设定的最小值,那大概率是阈值判定还没触发,或者参数实际没有生效。
另外注意,min的单位是毫秒,很多人习惯性地填10,以为是10秒,实际才10毫秒,等于无效配置。生产环境建议至少填1000以上。
4.3 失败计数表的记录为什么是空的
安装完辅助插件后,去查information_schema.CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS,发现空表。这不是故障,而是正常现象。这张表只在失败次数达到阈值后才会写入数据。如果失败次数没到阈值,MySQL认为这是"正常波动",不记录。
所以,如果你想测试这个插件的效果,需要连续失败超过阈值次数,再去查这张表。例如阈值是3,那你至少要试4次错误密码,第4次开始触发延迟,这个时候表里才会有一条记录。
4.4 误伤业务账号:高并发场景下的连接池堆积
这个坑很隐蔽,但一旦踩到就是事故级别的。假设你有一个Java应用,连接池配置了20个连接。正常情况下这20个连接是反复复用的,不会频繁建立新连接。但如果应用端的数据库密码被改错了,或者连接池里的某个连接因为网络抖动断开了,应用会尝试重新连接,此时如果集中重试,就会立刻触发Connection Control的延迟惩罚。
更麻烦的是,连接池在等待新连接时,不会把等待算作"业务超时",而是会堆积请求。当过了一段时间,惩罚延迟过去后,应用才能继续连库。但如果应用频繁重试,惩罚会不断累积。一旦到了这个状态,换对密码都救不了你,因为新连接会被延迟挡住。
在我们自己的一次压测事故中,就是因为运维改了数据库密码,但应用配置没同步更新,所有连接开始疯狂失败重连,最终触发Connection Control的最大延迟惩罚,导致该账号在一定时间内彻底无法建立新连接。解决方法是临时调大阈值或者临时关闭惩罚,等应用配置改对后再恢复。这个教训说明,参数调优一定要考虑监控告警和紧急降级方案。
4.5 与账号自动锁定的关系
常见的误解是:Connection Control会把账号锁定。其实它不会真正锁定账号,它只是让连接请求等待更长的时间。账号本身依然是可用状态,只是每次尝试都会被拖慢。如果你的需求是"失败次数达到N次就锁账号",那是另一个功能——CREATE USER语句里的FAILED_LOGIN_ATTEMPTS和PASSWORD_LOCK_TIME选项,或者是手动锁定账号。
我个人建议把两者结合:短平快的暴力破解靠Connection Control拖时间,长期撞库靠账号锁定来兜底。但账号锁定的阈值要设置得保守一点,因为一旦锁错,那就是真的锁死了,业务直接瘫痪。
5. 配套监控与周边防御的整体协同
5.1 如何从日志中识别主动攻击行为
Connection Control产生的延迟在MySQL错误日志中不会主动打印,除非你设置了额外的log_error_verbosity。因此,日常监控更多依赖MySQL提供的状态变量:
SHOW STATUS LIKE 'Connection_control_delay_generated';这个变量统计了插件累计产生延迟的次数。如果这个数字增长速度很快,说明频繁有连接触发惩罚,要么是攻击,要么是配置错误。建议接入监控系统,设置告警阈值,比如5分钟内增长超过100次,就触发告警。
更细粒度的方法是通过performance_schema来查看连接失败的具体来源。可惜的是,MySQL本身不直接记录每个失败连接的来源IP,这条链路更多依赖网络层日志或审计插件。我的建议是,如果有合规需求,直接上MySQL Enterprise Audit,否则开启通用日志可以选择性记录登录失败事件,但生产环境不建议全量开启,对性能影响太大。
5.2 Connection Control与fail2ban的协同分工
很多文章把Connection Control和fail2ban放在对立面比较,其实两者解决的问题层级完全不同。fail2ban工作在操作系统层,它盯的是系统认证日志,发现某个IP大量触发SSH密码错误,就直接在iptables层面封掉这个IP。Connection Control工作在MySQL层,它只关心MySQL内部的登录失败。
组合使用的价值在于:fail2ban解决了"同一IP扫全库所有账号"的问题,Connection Control解决了"多个IP轮流试同一个账号"的问题。单靠fail2ban,攻击者换一批IP还是能继续试;单靠Connection Control,攻击者用大量IP同时试,也能绕过单账号延迟。两者结合后就形成了纵深防御:外部IP被系统层封禁,内部账号被数据库层拖慢。
5.3 一套可以抄作业的部署模板
以下是一套我用于生产环境MySQL 8.0的模板化配置,供你参考:
[mysqld] # Connection Control plugin-load-add = connection_control.so plugin-load-add = connection_control_failed_login_attempts.so connection_control_failed_connections_threshold = 8 connection_control_min_connection_delay = 2000 connection_control_max_connection_delay = 3600000配合账号侧策略:
ALTER USER 'app_user'@'%' FAILED_LOGIN_ATTEMPTS 10 PASSWORD_LOCK_TIME 1;意思是连续失败10次后账号锁1天。对核心业务账号,阈值可以收紧到5次左右。
最后说一个日常运维的小习惯:每季度做一次账号权限复查时,把information_schema.CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS导出来看一眼,里面往往藏着很多正常情况下根本发现不了的扫描痕迹。比如某个从没见过的USERHOST出现了多次失败记录,那基本可以断定有人拿着破解好的字典在你的库上试。这时候不只是改密码的问题,还得溯源一下这个IP怎么进来的。插件给的是缓冲时间,真正的安全还得靠人的警惕性。