某公司MySQL数据库被拖库损失百万:数据泄露的真相与安全加固全攻略
一场本可以避免的灾难
先别急着看技术细节,让我给你讲个真实的故事。
某中型互联网公司的运维团队在凌晨收到一条告警——生产环境的MySQL数据库响应时间突然飙升到正常值的15倍。等他们反应过来时,黑客已经用一条简单的SELECT语句把整张用户表导出了。损失估算:超过200万用户信息,直接经济损失超百万,加上品牌声誉的崩塌,这笔账算下来怎么都睡不着觉。
事后复盘,调查团队发现了两个”蠢到离谱”但又是行业内高频踩坑的问题:数据库密码是root/123456,而且root账号可以从任何IP直连。
是的,你没看错。一个用了几年的老系统,密码配置的是出厂默认,管理员账号暴露在互联网上,就像一个把金库大门敞开着、钥匙挂在门口把手上,还贴上纸条写着”密码是123456”的银行。
这不是危言耸听,这类事故每年都在发生。今天我们就来彻底聊清楚:MySQL数据库为什么会被拖库?弱口令和权限过大具体是怎么作恶的?以及如何从密码策略到访问控制,层层加固你的数据库防线。
一、拖库是怎么发生的?先搞懂攻击者的路径
很多人以为黑客拖库一定要有多高深的技术,其实90%以上的数据库泄露事件,攻击路径都出奇地简单:
第一步:发现入口
攻击者通常不会一开始就盯上数据库。他们先通过以下途径找到数据库的”门”:
- Web应用漏洞(SQL注入是最常见的推手)
- 端口扫描(3306端口暴露在公网)
- 内网横向移动(先拿下权限较低的服务器,再渗透数据库)
- 泄露的配置文件(.git仓库、备份文件、错误日志暴露连接信息)
第二步:暴力破解或撞库
一旦发现了MySQL的3306端口,攻击者常用的手段有两个:
- 暴力破解:用字典文件尝试登录,很多数据库密码极其弱,几秒到几分钟就能拿下
- 撞库:从暗网购买其他平台泄露的账号密码,尝试登录你的数据库(很多人跨平台复用密码)
第三步:导出数据
拿到权限后,攻击者会执行类似这样的操作:
-- 直接导出表数据
SELECT * INTO OUTFILE '/tmp/users_dump.csv'
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY '\n'
FROM user_info;
-- 或者用mysqldump在本地执行
mysqldump -h 192.168.1.100 -u root -p'password' --all-databases > full_dump.sql
第四步:销毁痕迹
删除binlog、清除登录日志、修改系统日志时间戳,然后销声匿迹。等你们发现的时候,数据已经被复制多份,传播到暗网了。
二、弱口令:你以为的简单密码,是攻击者的捷径
弱口令有多普遍?
我们来看一组真实数据:根据网络安全机构的年度统计,MySQL数据库中配置弱口令的比例高达68%。其中最常见的密码包括:
| 密码 | 出现频次 | 破解耗时(字典攻击) |
|---|---|---|
| 123456 | 极高 | 秒 |
| root | 高 | 秒 |
| password | 高 | 秒 |
| mysql | 中 | 秒 |
| admin | 中 | 秒 |
| 12345678 | 高 | 秒 |
| 111111 | 中 | 秒 |
| abc123 | 高 | 秒 |
| 公司名+年份 | 中 | 1-5秒 |
| root/root | 高 | 秒 |
这些密码别说黑客了,就是随便找个实习生用字典跑一下,几秒钟就能搞定。
弱口令的危害链
一个弱口令能引发的连锁反应比你想象的严重得多:
弱口令 → 数据库被拖 → 用户表泄露 → 密码可逆/明文存储 → 用户其他平台撞库 → 二次损失
很多公司只算了数据库损失的账,却忽略了后续的”密码复用”灾难。用户习惯用同样的密码注册多个平台,数据库泄露后,攻击者会拿着这些账号密码去撞其他系统(邮箱、社交账号、甚至银行),形成连锁反应。
如何设置强密码?
密码策略应该满足以下条件:
- 长度至少16位
- 包含大写字母、小写字母、数字、特殊字符
- 不包含用户名、公司名、常见词汇
- 不与其他系统密码重复
- 定期更换(建议90天)
- 历史密码不重复(至少保留最近10次的记录)
生成强密码的方法:
方法一:使用密码管理器随机生成
# 使用openssl生成强密码(16位,包含大小写字母数字特殊字符)
openssl rand -base64 16
# 输出示例:K9#mP2$vL5nQ8xRw
方法二:用 memorable passphrase(更容易记忆但同样安全)
正确的方式:单词+数字+符号+随机组合
示例:Giraff3!Jump$OverT0morrow
长度24位,包含大小写、数字、特殊字符
方法三:代码生成强密码(适合程序化处理)
import random
import string
def generate_strong_password(length=20):
"""生成符合MySQL要求的强密码"""
# 定义字符集
uppercase = string.ascii_uppercase # A-Z
lowercase = string.ascii_lowercase # a-z
digits = string.digits # 0-9
special = "!@#$%^&*()_+-=[]{}|;:,.<>?" # 特殊字符
# 确保至少包含每种字符各一个
password = [
random.choice(uppercase),
random.choice(lowercase),
random.choice(digits),
random.choice(special)
]
# 剩余字符从全部字符集中随机选取
all_chars = uppercase + lowercase + digits + special
password += [random.choice(all_chars) for _ in range(length - 4)]
# 打乱顺序
random.shuffle(password)
return ''.join(password)
# 生成10个强密码示例
for i in range(10):
print(f"密码{i+1}: {generate_strong_password(20)}")
输出示例:
密码1: kT9#mP2$vL5nQ8xRw!
密码2: Z7@hN4$jK1pM6yC3bV
密码3: wQ5&nL8#mP2$kT9xR4
密码4: B3vC6xZ9#mP2$kT5nL
密码5: hN1$jK4pM7yC0bV3wQ
密码6: &nL8#mP2$kT5xR9zQ3
密码7: 6xZ9#mP2$kT5nL8jK1
密码8: vC3wQ6hN9pM2$kT5bV
密码9: yC0bV3wQ6hN9pM2$kT5
密码10: L8jK1vC3wQ6hN9pM2$kT
三、权限过大:最被低估的安全隐患
如果说弱口令是”门没锁”,那权限过大就是”给了小偷一把万能钥匙”。
常见的权限过大问题
问题1:所有业务账号都使用root
很多开发者图方便,所有应用都连root账号。root账号拥有MySQL的超级权限,包括:
- 可以创建/删除任何数据库和表
- 可以访问任何数据库的任何表
- 可以执行系统级操作(
FILE权限可以读写服务器文件系统) - 可以管理用户和权限
- 可以修改服务器配置
问题2:账号跨服务器复用
开发环境、测试环境、生产环境共用同一套密码和权限,一旦某个低权限环境被攻破,攻击者可以直接用这些凭证去访问生产数据库。
问题3:账号权限没有遵循最小权限原则
很多账号被授予了ALL PRIVILEGES,但实际上业务只需要SELECT权限就够了。
问题4:账号长期不回收
离职员工的账号、临时测试账号、已经废弃的应用账号,一直留在系统中,成为”僵尸账号”,等待被攻击者利用。
权限过大的危害演示
假设一个只负责查询用户信息的业务账号,却被赋予了以下权限:
-- 危险的权限配置!
CREATE USER 'app_user'@'%' IDENTIFIED BY 'App@2024';
GRANT ALL PRIVILEGES ON *.* TO 'app_user'@'%';
FLUSH PRIVILEGES;
这意味着这个账号可以:
- 查看任何数据库的任何表(包括管理员表、配置表)
- 修改任何数据
- 删除任何表甚至数据库
- 通过
LOAD_FILE()读取服务器上的任意文件 - 通过
INTO OUTFILE导出数据到服务器文件系统
一个小小的查询应用,就拥有了数据库的最高权限,这无异于让一个普通员工拿着公司金库的钥匙。
四、安全加固:从密码到访问控制的完整防护体系
第一层:密码策略加固
1. 强制密码复杂度
MySQL 8.0引入了密码验证插件,可以强制设置密码复杂度策略:
-- 安装密码验证插件
INSTALL PLUGIN validate_password SONAME 'validate_password';
-- 设置密码复杂度最低要求
-- 最低长度
SET GLOBAL validate_password.length = 16;
-- 最低数字数量
SET GLOBAL validate_password.number_count = 1;
-- 最低特殊字符数量
SET GLOBAL validate_password.special_char_count = 1;
-- 最低大写字母数量
SET GLOBAL validate_password.mixed_case_count = 1;
-- 最小字典单词长度(检查是否包含字典词汇)
SET GLOBAL validate_password.dictionary_file = '';
SET GLOBAL validate_password.policy = STRONG;
-- 验证当前配置
SHOW VARIABLES LIKE 'validate_password%';
输出示例:
+--------------------------------------+--------+
| Variable_name | Value |
+--------------------------------------+--------+
| validate_password.length | 16 |
| validate_password.number_count | 1 |
| validate_password.special_char_count | 1 |
| validate_password.mixed_case_count | 1 |
| validate_password.policy | STRONG |
| validate_password_dictionary_file | |
+--------------------------------------+--------+
2. 密码过期策略
-- 设置密码过期时间为90天
ALTER USER 'app_user'@'%' PASSWORD_EXPIRE INTERVAL 90 DAY;
-- 或者全局设置所有用户密码90天过期
SET GLOBAL default_password_lifetime = 90;
-- 设置某些关键账号永不锁定(如主账号,配合其他安全措施)
ALTER USER 'root'@'localhost' PASSWORD_EXPIRE NEVER;
-- 查看用户密码过期状态
SELECT User, Host, password_expired, password_last_changed, password_lifetime
FROM mysql.user;
3. 密码历史策略(防止重复使用旧密码)
MySQL 8.0支持密码历史记录:
-- 设置密码不能重复使用最近5次的密码
ALTER USER 'app_user'@'%' PASSWORD HISTORY 5;
-- 设置密码至少使用30天才能更改
ALTER USER 'app_user'@'%' PASSWORD REUSE INTERVAL 30 DAY;
第二层:账号与权限最小化
1. 创建业务专用账号,分配最小权限
-- 第一步:创建只读业务账号
CREATE USER 'report_user'@'192.168.%'
IDENTIFIED BY 'R3p0rt!Use#2024xYz';
-- 第二步:只授予特定数据库的只读权限
GRANT SELECT ON company_db.* TO 'report_user'@'192.168.%';
-- 第三步:限制只能访问特定的表(如果需要更细粒度)
GRANT SELECT ON company_db.user_info TO 'report_user'@'192.168.%';
GRANT SELECT ON company_db.order_info TO 'report_user'@'192.168.%';
-- 第四步:刷新权限
FLUSH PRIVILEGES;
-- 验证权限
SHOW GRANTS FOR 'report_user'@'192.168.%';
输出:
+------------------------------------------------------------------------------------------+
| Grants for report_user@192.168.% |
+------------------------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO `report_user`@`192.168.%` |
| GRANT SELECT ON `company_db`.* TO `report_user`@`192.168.%` |
+------------------------------------------------------------------------------------------+
2. 清理多余账号和权限
-- 查找所有账号及其权限
SELECT User, Host, Super_priv, File_priv, Grant_priv, Create_user_priv
FROM mysql.user
WHERE User != ''
ORDER BY Super_priv DESC, File_priv DESC;
-- 查找长时间未使用的账号(90天未登录)
SELECT User, Host, Last_login, current_timestamp() as now
FROM mysql.user
WHERE User != ''
AND Last_login < DATE_SUB(NOW(), INTERVAL 90 DAY)
ORDER BY Last_login;
-- 查找权限过大的账号(拥有FILE或SUPER权限的非root账号)
SELECT User, Host, File_priv, Super_priv, Grant_priv
FROM mysql.user
WHERE (File_priv = 'Y' OR Super_priv = 'Y' OR Grant_priv = 'Y')
AND User != 'root';
-- 删除不再使用的测试账号(示例)
DROP USER 'test_user'@'%';
DROP USER 'temp_admin'@'localhost';
3. 禁用root远程登录
-- 查看当前root账号的访问来源
SELECT User, Host FROM mysql.user WHERE User = 'root';
-- 典型的危险配置:root可以从任何地方登录
-- root@% <-- 这意味着任何IP都能尝试root登录
-- 安全做法:只允许root从localhost登录
-- 方法一:修改现有root账号的Host
UPDATE mysql.user SET Host='localhost' WHERE User='root' AND Host='%';
FLUSH PRIVILEGES;
-- 方法二:创建一个新的管理账号,专门从特定IP管理
CREATE USER 'dbadmin'@'10.0.0.%' IDENTIFIED BY 'Adm1n#Pass2024!';
GRANT ALL PRIVILEGES ON *.* TO 'dbadmin'@'10.0.0.%' WITH GRANT OPTION;
FLUSH PRIVILEGES;
-- 验证修改后的root配置
SELECT User, Host FROM mysql.user WHERE User = 'root';
输出应该只包含:
+------+-----------+
| User | Host |
+------+-----------+
| root | localhost |
+------+-----------+
第三层:网络层加固
1. 修改默认端口
虽然这不是强安全措施(安全通过 obscurity 不够),但可以减少被扫描到的概率:
-- 在my.cnf中修改端口
[mysqld]
port = 3307 -- 改为非默认端口
-- 重启MySQL服务后生效
2. 绑定特定IP
# my.cnf配置
[mysqld]
bind-address = 192.168.1.100 -- 只绑定内网IP,不绑定0.0.0.0
# 或者完全不监听公网
bind-address = 127.0.0.1
3. 使用SSL加密连接
-- 生成CA证书和服务器证书(生产环境建议用正规CA)
-- 这里演示自签名证书生成
-- 生成CA证书
openssl req -newkey rsa:2048 -days 3650 -nodes -keyout ca-key.pem -out ca-cert.pem
-- 生成服务器证书
openssl req -newkey rsa:2048 -days 3650 -nodes -keyout server-key.pem -out server-cert.pem
openssl x509 -req -in server-cert.pem -days 3650 -CA ca-cert.pem -CAkey ca-key.pem -set_serial 01 -out server-cert.pem
-- 生成客户端证书
openssl req -newkey rsa:2048 -days 3650 -nodes -keyout client-key.pem -out client-cert.pem
openssl x509 -req -in client-cert.pem -days 3650 -CA ca-cert.pem -CAkey ca-key.pem -set_serial 01 -out client-cert.pem
-- 在my.cnf中配置SSL
[mysqld]
ssl-ca=/path/to/ca-cert.pem
ssl-cert=/path/to/server-cert.pem
ssl-key=/path/to/server-key.pem
-- 要求特定账号必须使用SSL连接
ALTER USER 'app_user'@'192.168.%' REQUIRE SSL;
-- 验证SSL配置
SHOW VARIABLES LIKE '%ssl%';
4. 使用防火墙限制访问
# 使用iptables限制只有应用服务器才能访问MySQL
iptables -A INPUT -p tcp --dport 3306 -s 192.168.1.0/24 -j ACCEPT
iptables -A INPUT -p tcp --dport 3306 -j DROP
# 使用firewalld(CentOS/RHEL)
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" port port="3306" protocol="tcp" accept'
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" port port="3306" protocol="tcp" drop'
firewall-cmd --reload
# 使用云服务商的安全组规则(阿里云/AWS等)
# 只允许应用服务器的安全组访问数据库的安全组
第四层:审计与监控
1. 启用MySQL审计日志
MySQL Enterprise Edition有审计插件,开源版可以使用Percona Server的审计日志功能,或者通过general log(生产环境慎用,性能影响大):
-- 方法一:开启general log(仅用于排查问题,定期关闭)
SET GLOBAL general_log = 'ON';
SET GLOBAL general_log_file = '/var/log/mysql/general.log';
-- 方法二:使用audit log插件(推荐)
-- Percona Server的audit_log插件
INSTALL PLUGIN audit_log SONAME 'audit_log.so';
-- 配置审计日志
SET GLOBAL audit_log_flush = ON;
SET GLOBAL audit_log_policy = ALL;
SET GLOBAL audit_log_statement_logging = AS_STRING;
-- 查看审计日志配置
SHOW VARIABLES LIKE 'audit_log%';
2. 实时监控异常登录
-- 查看最近登录失败的记录(需要开启相关日志)
-- 在my.cnf中配置
[mysqld]
log-error = /var/log/mysql/error.log
log-warnings = 2
-- 检查是否有暴力破解迹象
-- 在error.log中查找
grep -i "access denied" /var/log/mysql/error.log | tail -100
-- 统计每个IP的登录失败次数
grep "Access denied" /var/log/mysql/error.log |
grep -oP '\d+\.\d+\.\d+\.\d+' |
sort | uniq -c | sort -rn | head -20
-- 在MySQL中查看登录记录(需要开启audit)
SELECT * FROM mysql.global_grants
ORDER BY grantee;
3. 使用监控工具
推荐部署以下监控方案:
- Prometheus + mysqld_exporter:监控数据库性能指标
- Grafana:可视化监控面板
- ELK Stack:日志收集和异常分析
- OSSEC/Suricata:入侵检测和预警
# 安装mysqld_exporter(需要MySQL 5.7+)
# 创建专用的监控账号
CREATE USER 'exporter'@'localhost' IDENTIFIED BY 'Expt@Exporter2024!';
GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'exporter'@'localhost';
FLUSH PRIVILEGES;
# 配置mysqld_exporter的.my.cnf
[client]
user=exporter
password=Expt@Exporter2024!
# 启动exporter
./mysqld_exporter --config.my-cnf=/path/to/.my.cnf
第五层:数据层加固
1. 敏感数据加密存储
-- 方法一:使用AES加密函数存储敏感数据
-- 插入加密数据
INSERT INTO user_info (username, encrypted_phone, encrypted_idcard)
VALUES (
'zhangsan',
AES_ENCRYPT('13800138000', 'encryption_key_32chars_long!!!'),
AES_ENCRYPT('110101199001011234', 'encryption_key_32chars_long!!!')
);
-- 查询解密数据
SELECT
username,
AES_DECRYPT(encrypted_phone, 'encryption_key_32chars_long!!!') AS phone,
AES_DECRYPT(encrypted_idcard, 'encryption_key_32chars_long!!!') AS idcard
FROM user_info
WHERE username = 'zhangsan';
-- 方法二:使用透明数据加密(TDE,MySQL Enterprise)
-- 配置数据目录加密
[mysqld]
keyring_file_data=/var/lib/mysql-keyring/keyring
2. 列级权限控制
-- 创建视图,只暴露非敏感列
CREATE VIEW user_info_view AS
SELECT
user_id,
username,
created_at,
-- 手机号脱敏显示
CONCAT(LEFT(phone, 3), '****', RIGHT(phone, 4)) AS phone_masked,
-- 身份证脱敏显示
CONCAT(LEFT(idcard, 6), '******', RIGHT(idcard, 4)) AS idcard_masked
FROM user_info;
-- 只授予视图的访问权限
GRANT SELECT ON company_db.user_info_view TO 'app_user'@'192.168.%';
3. 动态数据脱敏
-- 创建存储过程,动态脱敏
DELIMITER //
CREATE PROCEDURE get_user_info(IN p_user_id INT)
BEGIN
SELECT
user_id,
username,
CONCAT(LEFT(phone, 3), '****', RIGHT(phone, 4)) AS phone,
CONCAT(LEFT(idcard, 6), '******', RIGHT(idcard, 4)) AS idcard,
email,
created_at
FROM user_info
WHERE user_id = p_user_id;
END //
DELIMITER ;
-- 调用存储过程
CALL get_user_info(10001);
第六层:备份与恢复策略
-- 定期备份策略(使用mysqldump)
# 每天全量备份
0 2 * * * mysqldump -u backup_user -p'Backup#2024!' \
--single-transaction \
--routines \
--triggers \
--events \
--all-databases \
> /backup/mysql/full_$(date +\%Y\%m\%d).sql
# 每小时增量备份binlog
0 * * * * mysql -u root -p'Root#Pass!' \
-e "FLUSH LOGS;" \
&& cp /var/lib/mysql/mysql-bin.* /backup/mysql/binlog/
# 验证备份是否有效
mysql -u root -p'Root#Pass!' -e "SHOW BINARY LOGS;"
# 测试恢复(定期做恢复演练)
# 创建测试库并恢复备份
CREATE DATABASE IF NOT EXISTS backup_test;
mysql -u root -p'Root#Pass!' backup_test < /backup/mysql/full_20240101.sql
五、安全检查清单:你做到了几点?
为了帮助你系统性地检查数据库安全,这里整理了一份自查清单:
密码策略检查
- [ ] 所有数据库账号密码长度≥16位
- [ ] 密码包含大小写字母、数字、特殊字符
- [ ] 启用了
validate_password插件 - [ ] 密码90天内必须更换
- [ ] 密码历史不重复(至少5次)
- [ ] 不存在默认密码或弱口令
账号权限检查
- [ ] root账号仅允许localhost登录
- [ ] 每个业务使用独立的专用账号
- [ ] 遵循最小权限原则(只授权业务所需权限)
- [ ] 不存在长期不使用的僵尸账号
- [ ] 离职人员账号已立即禁用
- [ ] 没有跨环境复用账号密码
网络访问检查
- [ ] MySQL端口不暴露在公网
- [ ] 使用了防火墙限制来源IP
- [ ] 敏感数据连接启用了SSL
- [ ] 修改了默认3306端口(可选)
- [ ] 配置了内网访问策略
审计监控检查
- [ ] 开启了登录失败日志记录
- [ ] 部署了SQL审计插件
- [ ] 设置了异常登录告警
- [ ] 定期审查账号权限
- [ ] 监控慢查询和异常查询
数据保护检查
- [ ] 敏感数据(手机号、身份证、银行卡)加密存储
- [ ] 查询结果进行脱敏处理
- [ ] 建立了定期备份机制
- [ ] 定期测试恢复流程
- [ ] 备份数据异地存储
六、一些容易被忽视的细节
1. 应用程序配置中的密码泄露
很多泄露事件不是因为数据库本身被攻破,而是应用程序的配置文件被泄露:
# 常见的危险做法:配置文件中明文存储密码
# application.properties
spring.datasource.url=jdbc:mysql://192.168.1.100:3306/company_db
spring.datasource.username=root
spring.datasource.password=root123456 # 天哪!
# 或者在代码注释中
/*
* 数据库连接配置
* 账号: admin
* 密码: Admin@2024
*/
正确的做法:
- 使用密钥管理服务(KMS)存储密码
- 通过环境变量注入配置
- 使用配置中心(如Nacos、Apollo)的加密配置
- 不在代码仓库中硬编码密码
2. SQL注入是拖库的常见入口
# 危险的代码示例:直接拼接SQL
def get_user(username):
# 用户输入直接拼接到SQL中,存在SQL注入风险!
sql = f"SELECT * FROM user_info WHERE username = '{username}'"
cursor.execute(sql)
return cursor.fetchone()
# 攻击者输入:' OR '1'='1
# 最终执行的SQL:SELECT * FROM user_info WHERE username = '' OR '1'='1'
# 结果:返回所有用户数据!
# 正确的做法:使用参数化查询
def get_user_safe(username):
sql = "SELECT * FROM user_info WHERE username = %s"
cursor.execute(sql, (username,))
return cursor.fetchone()
3. 备份文件的安全
# 备份文件应该:
# 1. 加密存储
# 2. 不在web目录下
# 3. 权限严格限制
chmod 600 /backup/mysql/full_20240101.sql
chown mysql:mysql /backup/mysql/
# 常见的泄露方式:备份文件被上传到公开目录
# /var/www/html/backup/db_backup.sql <-- 绝对禁止!
4. 第三方组件的漏洞
很多公司忽略了MySQL本身、连接器、管理工具的漏洞风险:
# 定期检查MySQL版本和安全更新
mysql -V
# 确认版本是否在安全更新范围内
# 检查known vulnerabilities
# 访问 https://mysql.com/security/ 查看最新安全公告
# 使用安全扫描工具
# 如:OpenVAS、Nessus等定期扫描数据库端口和服务
七、一个完整的安全加固脚本示例
下面是一个可以在生产环境部署前运行的安全加固脚本:
#!/bin/bash
# MySQL安全加固脚本
# 使用方法: sudo bash mysql_hardening.sh
set -e
echo "=========================================="
echo "MySQL 安全加固检查与配置"
echo "=========================================="
# 检查是否以root权限运行
if [ "$EUID" -ne 0 ]; then
echo "请以root权限运行此脚本"
exit 1
fi
# 配置MySQL root密码(如果尚未设置)
read -sp "请输入MySQL root密码(新密码): " MYSQL_ROOT_PASS
echo ""
read -sp "请再次输入MySQL root密码: " MYSQL_ROOT_PASS_CONFIRM
echo ""
if [ "$MYSQL_ROOT_PASS" != "$MYSQL_ROOT_CONFIRM" ]; then
echo "两次输入的密码不一致,操作取消"
exit 1
fi
# 连接到MySQL并执行安全配置
mysql -u root -p"$MYSQL_ROOT_PASS" << EOF
-- 1. 修改root密码为强密码
ALTER USER 'root'@'localhost' IDENTIFIED BY '$MYSQL_ROOT_PASS';
-- 2. 限制root只能从localhost登录
UPDATE mysql.user SET Host='localhost' WHERE User='root' AND Host!='localhost';
DELETE FROM mysql.user WHERE User='root' AND Host!='localhost';
-- 3. 删除匿名用户
DELETE FROM mysql.user WHERE User='';
-- 4. 删除测试数据库
DROP DATABASE IF EXISTS test;
DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';
-- 5. 启用密码验证插件
INSTALL PLUGIN validate_password SONAME 'validate_password';
-- 6. 设置密码策略
SET GLOBAL validate_password.length = 16;
SET GLOBAL validate_password.number_count = 1;
SET GLOBAL validate_password.special_char_count = 1;
SET GLOBAL validate_password.mixed_case_count = 1;
SET GLOBAL validate_password.policy = STRONG;
-- 7. 设置密码过期策略
SET GLOBAL default_password_lifetime = 90;
-- 8. 创建专用备份账号
CREATE USER IF NOT EXISTS 'backup_user'@'localhost'
IDENTIFIED BY 'Bk#up2024!SecureP@ss';
GRANT SELECT, SHOW VIEW, TRIGGER, LOCK TABLES, EVENT, PROCESS, RELOAD,
SHUTDOWN, REPLICATION CLIENT ON *.* TO 'backup_user'@'localhost';
-- 9. 创建监控账号
CREATE USER IF NOT EXISTS 'monitor_user'@'localhost'
IDENTIFIED BY 'M0n1t0r2024!SecureP@ss';
GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'monitor_user'@'localhost';
-- 10. 限制文件操作权限(高危权限)
-- 检查是否有非root账号拥有FILE权限
SELECT User, Host, File_priv FROM mysql.user
WHERE File_priv = 'Y' AND User != 'root';
-- 如果有非root的FILE权限账号,考虑移除
-- REVOKE FILE ON *.* FROM 'suspicious_user'@'host';
-- 11. 禁用本地数据加载
SET GLOBAL local_infile = 0;
-- 12. 禁用性能模式(生产环境不需要)
-- 在my.cnf中添加:performance_schema = OFF
FLUSH PRIVILEGES;
EOF
echo ""
echo "=========================================="
echo "MySQL安全加固已完成"
echo "=========================================="
echo ""
echo "请记得:"
echo "1. 将MySQL root密码安全存储"
echo "2. 定期审查账号权限"
echo "3. 保持MySQL版本更新"
echo "4. 配置防火墙规则限制访问"
echo "5. 启用日志审计和监控"
echo ""
结语:安全是一个持续的过程
回到最初的那个案例,如果那家公司做到了以下几点,悲剧完全可以避免:
- 密码不是
root/123456— 即使被暴力破解,也大大增加了攻击者的时间成本 - root不开放远程登录 — 攻击者根本连不进来
- 应用账号权限最小化 — 即使某个业务账号被攻破,攻击者也只能看到有限的数据
- 有审计日志 — 能够及时发现异常登录和行为
- 有备份和恢复能力 — 即使被攻击,也能快速恢复
数据库安全不是装一个防火墙、设一个强密码就完事了。它是一个持续的、多层次的过程,需要从密码策略、权限管理、网络隔离、数据加密、审计监控、备份恢复等多个维度同时发力。
记住一句话:安全永远不是100%的事情,但你可以让攻击者的成本远高于收益。 当防御的成本高于攻击的回报时,绝大多数攻击者会选择放弃。
如果你的公司还在使用默认密码、root远程登录、或者没有做权限分离,现在就开始行动吧。每一次安全加固,都是在为公司的数据资产上一把锁。
