引言:那声刺耳的警报
2023年底,某知名电商平台发生了一起震惊业界的数据库泄露事件。攻击者通过一个未关闭的3306端口,使用默认的空密码登录MySQL,轻松获取了平台核心数据。事后分析发现,该漏洞本可在部署阶段就被堵住。
作为技术人员,我们常把安全视为”锦上添花”,但这次事件告诉我们:安全是基石,不是装饰。今天,我将带你从零开始,一步步完成MySQL的全面安全加固,让同样的悲剧不再重演。
第一部分:事件复盘——我们该如何汲取教训?
1.1 事件时间线还原
让我们先还原事件的关键节点:
[2023-11-15 02:14:33] 攻击源IP: 45.32.77.88
[2023-11-15 02:14:35] 扫描检测到3306端口开放
[2023-11-15 02:14:36] 尝试MySQL默认空密码登录
[2023-11-15 02:14:36] 登录成功!
[2023-11-15 02:14:45] 执行 SHOW DATABASES;
[2023-11-15 02:15:02] 执行 SELECT * FROM users;
[2023-11-15 02:15:30] 执行 SELECT * FROM payment_info;
[2023-11-15 02:16:00] 使用 mysqldump 导出全库
[2023-11-15 02:20:00] 数据外传完成
1.2 根因分析
通过后续审计,我们发现了5个致命问题:
| 问题类型 | 具体表现 | 严重程度 |
|---|---|---|
| 端口暴露 | 3306端口直接对公网开放 | 🔴 极高 |
| 空密码 | root用户无密码或空密码 | 🔴 极高 |
| 弱口令 | 普通用户密码为”123456”、”admin”等 | 🟠 高 |
| 权限过大 | 业务账号拥有DROP、CREATE权限 | 🟠 高 |
| 版本过旧 | MySQL 5.6,已停止安全支持 | 🟡 中 |
1.3 我们学到的教训
第一,不要假设”内网就安全”。 攻击者往往通过Web应用漏洞进入内网,然后通过横向移动寻找数据库。
第二,最小权限原则不是口号,是铁律。 业务账号只需要它能完成工作的最少权限。
第三,补丁要跟进。 MySQL 5.6在2021年就已停止支持,但很多公司仍在生产环境使用。
第二部分:环境准备——我们需要什么?
2.1 目标环境
在开始之前,让我们明确我们的目标环境:
- 操作系统:Ubuntu 22.04 LTS 或 CentOS 8+
- MySQL版本:MySQL 8.0.32+(推荐)或 MySQL 5.7.44+(最低)
- 访问权限:root权限或sudo权限
- 网络环境:有公网IP或可配置防火墙的环境
2.2 检查当前状态
首先,我们需要了解当前MySQL的安装情况和状态:
# 检查MySQL是否已安装
mysql --version
# 检查MySQL服务状态
sudo systemctl status mysql
# 检查MySQL监听端口
sudo netstat -tlnp | grep mysql
# 或
sudo ss -tlnp | grep mysql
# 检查MySQL配置文件位置
sudo find /etc -name "my.cnf" 2>/dev/null
典型输出应该是:
mysql Ver 8.0.32-0ubuntu0.22.04.2 for Linux on x86_64 ((Ubuntu))
● mysql.service - MySQL Community Server
Loaded: loaded (/lib/systemd/system/mysql.service; enabled)
Active: active (running) since Mon 2023-11-20 10:30:00 CST; 2 days ago
tcp6 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN 1234/mysql
注意:如果监听地址是 0.0.0.0:3306 或 :::3306,说明MySQL对所有网络接口开放,这是高危状态!
2.3 记录当前配置
在加固前,先备份当前配置,以便后续对比:
# 备份MySQL配置
sudo cp /etc/mysql/mysql.conf.d/mysqld.cnf /etc/mysql/mysql.conf.d/mysqld.cnf.bak.$(date +%Y%m%d)
# 备份用户表
mysql -u root -p -e "SELECT host, user, authentication_string, plugin FROM mysql.user;" > /tmp/mysql_users_backup.sql
# 记录当前版本和配置
mysql -u root -p -e "SELECT VERSION(); SHOW VARIABLES LIKE '%version%';" > /tmp/mysql_version_backup.sql
第三部分:关闭远程高危端口——第一道防线
3.1 理解风险
MySQL默认端口是3306,这个端口被扫描器盯上无数次了。攻击工具如nmap、masscan会自动扫描公网IP的3306端口,一旦发现就尝试连接。
关键认知:生产数据库不应该直接暴露在互联网上。
3.2 方法一:修改绑定地址(最推荐)
MySQL配置文件控制着它监听哪些网络接口。我们需要让它只监听本地回环地址。
编辑MySQL配置文件:
# Ubuntu/Debian系统
sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf
# CentOS/RHEL系统
sudo nano /etc/my.cnf
找到 [mysqld] 部分,修改或添加以下配置:
[mysqld]
# 只监听本地,不监听外部网络
bind-address = 127.0.0.1
# 或者如果需要使用IPv6
# bind-address = ::1
# 如果确实需要远程访问,可以使用特定IP
# bind-address = 192.168.1.100
注意:如果你使用云数据库(如AWS RDS、阿里云RDS),这个配置可能在控制台管理,需要登录云服务商的Web界面修改。
3.3 方法二:使用防火墙规则(第二道防线)
即使修改了绑定地址,我们仍然需要防火墙作为补充保护。
对于Ubuntu(使用UFW):
# 查看当前防火墙状态
sudo ufw status verbose
# 默认拒绝所有入站连接
sudo ufw default deny incoming
# 允许SSH(重要!否则你会把自己锁在外面)
sudo ufw allow 22/tcp
# 只允许特定IP访问3306端口
# 假设你的应用服务器IP是192.168.1.50
sudo ufw allow from 192.168.1.50 to any port 3306
# 禁用默认允许的3306端口(如果之前开过)
sudo ufw delete allow 3306
# 启用防火墙
sudo ufw enable
# 再次检查状态
sudo ufw status verbose
对于CentOS/RHEL(使用firewalld):
# 查看当前防火墙状态
sudo firewall-cmd --state
# 允许SSH
sudo firewall-cmd --permanent --add-service=ssh
# 只允许特定IP访问3306
sudo firewall-cmd --permanent --add-rich-rule="rule family='ipv4' source address='192.168.1.50/32' port port='3306' protocol='tcp' accept"
# 重新加载防火墙
sudo firewall-cmd --reload
# 检查规则
sudo firewall-cmd --list-all
对于使用iptables的系统:
# 允许SSH
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
# 只允许特定IP访问MySQL
sudo iptables -A INPUT -p tcp -s 192.168.1.50 --dport 3306 -j ACCEPT
# 拒绝其他所有到3306的连接
sudo iptables -A INPUT -p tcp --dport 3306 -j DROP
# 保存规则
sudo iptables-save | sudo tee /etc/iptables/rules.v4
3.4 验证端口是否关闭
修改配置后,重启MySQL服务:
sudo systemctl restart mysql
然后验证:
# 检查MySQL监听状态
sudo netstat -tlnp | grep mysql
# 或
sudo ss -tlnp | grep mysql
# 应该看到类似这样(只监听本地):
# tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN 1234/mysql
# 从本地测试连接
mysql -u root -p -h 127.0.0.1
# 尝试从外部连接(应该失败)
# 在其他机器上执行:
# mysql -u root -p -h 你的服务器IP
# 预期结果:Connection refused 或 Timeout
重要提示:如果你需要远程管理MySQL(如使用Navicat、MySQL Workbench),请确保你的管理IP在防火墙允许列表中,或者使用SSH隧道。
第四部分:禁用空密码和弱口令——第二道防线
4.1 空密码的危害
空密码意味着任何人都可以无需验证直接登录。在攻击者扫描到开放端口后,空密码是最简单的突破口。
4.2 检查空密码用户
-- 登录MySQL
mysql -u root -p
-- 检查空密码用户
SELECT host, user, authentication_string, plugin
FROM mysql.user
WHERE authentication_string IS NULL OR authentication_string = '';
典型输出可能显示root或其他用户有空密码:
+-----------+------------------+---------------------+-----------------------+
| host | user | authentication_string | plugin |
+-----------+------------------+---------------------+-----------------------+
| localhost | root | NULL | mysql_native_password |
| % | admin | NULL | mysql_native_password |
+-----------+------------------+---------------------+-----------------------+
4.3 设置强密码
方法一:使用ALTER USER(推荐)
-- 为root设置强密码
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'Str0ng!P@ssw0rd#2024';
-- 为其他空密码用户设置密码
ALTER USER 'admin'@'%' IDENTIFIED WITH mysql_native_password BY 'Adm1n!P@ss#2024';
-- 刷新权限
FLUSH PRIVILEGES;
方法二:使用SET PASSWORD
-- 修改当前用户的密码
SET PASSWORD FOR 'root'@'localhost' = PASSWORD('Str0ng!P@ssw0rd#2024');
-- 刷新权限
FLUSH PRIVILEGES;
4.4 密码复杂度策略
MySQL 8.0+支持密码验证插件,可以强制密码复杂度:
-- 安装密码验证插件
INSTALL COMPONENT 'file://component_validate_password';
-- 检查当前密码策略
SHOW VARIABLES LIKE 'validate_password%';
默认配置已经相当严格。如果需要调整:
-- 设置最低密码长度(建议12位以上)
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;
-- 检查策略是否生效
SHOW VARIABLES LIKE 'validate_password%';
典型输出:
+--------------------------------------+--------+
| Variable_name | Value |
+--------------------------------------+--------+
| validate_password.check_user_name | ON |
| validate_password.counter_weight | 2 |
| validate_password.length | 12 |
| validate_password.mixed_case_count | 1 |
| validate_password.number_count | 1 |
| validate_password.policy | MEDIUM |
| validate_password.special_char_count | 1 |
| validate_password.username_length | 12 |
+--------------------------------------+--------+
4.5 检查弱口令
我们可以使用一些工具来检查弱口令,但请注意:只在授权环境下进行测试!
方法一:手动检查常见弱口令
-- 创建一个测试表来检查常见弱口令
CREATE TABLE IF NOT EXISTS weak_password_check (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(64),
password_hash VARCHAR(255),
risk_level VARCHAR(20)
);
-- 插入常见弱口令的哈希值进行对比
-- 注意:以下仅用于演示,实际测试请使用专业工具
INSERT INTO weak_password_check (username, password_hash, risk_level)
VALUES
('root', '*2470809C584BBDETA8E4E15EBFF80B5E24419542', 'HIGH'), -- 123456
('admin', '*6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9', 'HIGH'), -- admin
('test', '*A731A7FB0A64F32F34DE114D0834F0C97B66F106', 'HIGH'); -- test
-- 查询哪些用户使用了弱口令
SELECT u.user, u.host, w.risk_level
FROM mysql.user u
LEFT JOIN weak_password_check w ON u.authentication_string = w.password_hash
WHERE w.risk_level IS NOT NULL;
方法二:使用专业工具(建议在测试环境)
# 使用 Hydra 进行密码强度测试(仅限授权环境)
# 注意:必须在获得明确授权后进行!
hydra -l root -P /usr/share/wordlists/rockyou.txt mysql://127.0.0.1
# 或使用 ncrack
ncrack -u root -P /usr/share/wordlists/rockyou.txt mysql://127.0.0.1
4.6 删除不必要的用户
-- 查看所有用户
SELECT user, host FROM mysql.user;
-- 删除匿名用户
DROP USER ''@'localhost';
DROP USER ''@'hostname';
-- 删除测试用户(如果存在)
DROP USER 'test'@'localhost';
DROP USER 'test'@'%';
-- 删除不再使用的用户
DROP USER 'old_admin'@'%';
-- 刷新权限
FLUSH PRIVILEGES;
第五部分:配置权限最小化——第三道防线
5.1 权限模型概述
MySQL的权限系统基于”用户@主机”的组合。每个用户可以在不同主机上有不同的权限。
用户 主机 含义
root localhost 只有本地可以以root登录
app % 任何主机都可以以app用户登录
app 192.168.1.% 只有192.168.1网段可以以app用户登录
5.2 创建最小权限用户
步骤1:创建专用应用用户
-- 创建应用用户,只允许从应用服务器IP连接
CREATE USER 'app_user'@'192.168.1.50' IDENTIFIED BY 'App!User#2024';
-- 创建数据库
CREATE DATABASE IF NOT EXISTS ecommerce_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 只授予SELECT, INSERT, UPDATE, DELETE权限
GRANT SELECT, INSERT, UPDATE, DELETE ON ecommerce_db.* TO 'app_user'@'192.168.1.50';
-- 刷新权限
FLUSH PRIVILEGES;
步骤2:创建只读用户(用于报表)
-- 创建只读用户
CREATE USER 'readonly_user'@'192.168.1.%' IDENTIFIED BY 'Read!Only#2024';
-- 只授予SELECT权限
GRANT SELECT ON ecommerce_db.* TO 'readonly_user'@'192.168.1.%';
-- 刷新权限
FLUSH PRIVILEGES;
步骤3:创建备份用户
-- 创建备份用户
CREATE USER 'backup_user'@'localhost' IDENTIFIED BY 'B@ckup#2024';
-- 授予备份所需权限
GRANT SELECT, SHOW VIEW, TRIGGER, LOCK TABLES, EVENT, PROCESS ON *.* TO 'backup_user'@'localhost';
-- 刷新权限
FLUSH PRIVILEGES;
5.3 收回过度权限
”`sql – 查看当前所有用户的权限 SHOW GRANTS FOR ‘app_user’@‘192.168.1.50’;
– 收回不需要的权限 REVOKE DROP, CREATE, ALTER ON ecommerce_db.* FROM ‘app_user’@‘19
