363 lines
12 KiB
Markdown
363 lines
12 KiB
Markdown
---
|
||
tags: [MySQL, 安全加固, SSL/TLS, SQL Injection, ACL]
|
||
create time: 2026-05-16 00:00
|
||
---
|
||
|
||
# 安全加固
|
||
|
||
## 概述
|
||
|
||
数据库安全涉及访问控制、传输加密、数据保护和防御攻击等多个层面。MySQL 内置了丰富的安全机制,但默认配置往往不够严格。本节梳理生产环境必须做的安全加固措施。
|
||
|
||
## 最小权限原则
|
||
|
||
> [!QUESTION] 思考
|
||
> 为什么应用账号不应该拥有 `DROP` 或 `ALTER` 权限?如果业务确实需要建表,应该怎么设计?
|
||
|
||
```sql
|
||
-- ❌ 最差实践:应用直接用 root 连接(常见于开发环境)
|
||
GRANT ALL PRIVILEGES ON *.* TO 'app_user'@'%';
|
||
|
||
-- ✅ 正确做法:最小权限 + 限制来源 IP
|
||
CREATE USER 'app_user'@'10.0.1.%' IDENTIFIED BY 'strong_password_here';
|
||
GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app_user'@'10.0.1.%';
|
||
-- ⚠️ 不给 CREATE、DROP、ALTER、FILE、PROCESS、SUPER 等管理权限
|
||
FLUSH PRIVILEGES;
|
||
```
|
||
|
||
### MySQL 角色系统(8.0+)
|
||
|
||
> [!TIP] 角色 vs 直接授权
|
||
> 当用户数量增多时,逐个给用户赋权难以维护。角色可以理解为"权限模板",给角色授权后再把角色分配给用户。
|
||
|
||
```sql
|
||
-- 创建角色(权限模板)
|
||
CREATE ROLE 'app_reader', 'app_writer', 'app_admin';
|
||
|
||
-- 按角色分配权限
|
||
GRANT SELECT ON app_db.* TO 'app_reader';
|
||
GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app_writer';
|
||
GRANT ALL ON app_db.* TO 'app_admin';
|
||
|
||
-- 给用户分配角色
|
||
GRANT 'app_reader', 'app_writer' TO 'app_user'@'%';
|
||
SET DEFAULT ROLE ALL TO 'app_user'@'%'; -- 登录后自动激活这些角色
|
||
```
|
||
|
||
> [!NOTE] 高危权限清单(绝对不可滥用)
|
||
> - **FILE**: 可以读取服务器上的任意文件(`LOAD DATA INFILE`)→ 可读取 `/etc/shadow`
|
||
> - **PROCESS**: 可以看到所有用户的 running query → 泄露业务逻辑和敏感数据
|
||
> - **SUPER**: 可以修改全局配置(包括关闭 SSL 要求)→ 可能绕过多层安全防御
|
||
> - **SHUTDOWN**: 可以直接关闭数据库 → 造成停机
|
||
> - ** grant option**: 可以将自己拥有的权限授予他人 → 权限扩散
|
||
>
|
||
> 应用账号绝对不应该拥有以上任何权限。
|
||
|
||
## 账户初始化与清理
|
||
|
||
> [!QUESTION] 你以为干净的数据库,真的干净吗?
|
||
> MySQL 初始化后会默认创建一些不安全的账户和数据库,很多团队忽略了这一步。
|
||
|
||
```sql
|
||
-- 初始化后必须执行的操作:
|
||
DROP DATABASE IF EXISTS test; -- 默认的 test 库任何人都可以访问
|
||
DROP USER ''@'localhost'; -- 删除匿名账户(无密码即可登录)
|
||
DROP USER 'root'@'%' ; -- 删除 root 的远程访问权限
|
||
|
||
-- 检查残留账户
|
||
SELECT user, host, plugin FROM mysql.user WHERE user = '';
|
||
|
||
-- 确保 root 只能从本地登录
|
||
RENAME USER 'root'@'%' TO 'root'@'localhost';
|
||
```
|
||
|
||
## 密码安全
|
||
|
||
> [!QUESTION] 你的密码策略能挡住暴力破解吗?
|
||
> `123456`、`password`、`admin123` 这类弱口令是黑客的首选攻击目标。
|
||
|
||
```sql
|
||
-- 启用密码强度验证插件
|
||
INSTALL COMPONENT 'file://component_validate_password';
|
||
|
||
-- 配置密码策略
|
||
SET GLOBAL validate_password.policy = MEDIUM; -- LOW/MEDIUM/STRONG
|
||
SET GLOBAL validate_password.length = 12; -- 最小长度建议 ≥ 16
|
||
SET GLOBAL validate_password.mixed_case_count = 1; -- 大小写混合
|
||
SET GLOBAL validate_password.number_count = 1; -- 数字要求
|
||
SET GLOBAL validate_password.special_char_count = 1; -- 特殊字符
|
||
|
||
-- 强制现有用户使用强密码并定期轮换
|
||
ALTER USER 'app_user'@'%' PASSWORD EXPIRE INTERVAL 90 DAY;
|
||
-- 90 天强制更换密码
|
||
|
||
-- 服务账号可以关闭过期(避免定时任务因密码过期而失败)
|
||
ALTER USER 'app_user'@'%' PASSWORD EXPIRE NEVER;
|
||
```
|
||
|
||
## SQL 注入防护
|
||
|
||
> [!WARNING] 核心原则
|
||
> SQL 注入的本质是**将用户输入当作 SQL 代码来执行**。参数化查询通过驱动层发送参数,服务端将其视为纯数据而非可执行语句。
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
A["恶意输入: admin' OR '1'='1"] --> B{是否参数化?}
|
||
B -->|❌ 字符串拼接| C["最终 SQL: SELECT * FROM users WHERE name = 'admin' OR '1'='1'"]
|
||
C --> D["⚠️ 返回所有记录"]
|
||
B -->|✅ 占位符 ?| E["driver 发送参数作为数据"]
|
||
E --> F["最终 SQL: SELECT * FROM users WHERE name = ? \n(参数值: admin' OR '1'='1")"]
|
||
F --> G["✅ 找不到匹配,返回空"]
|
||
|
||
style D fill:#FF6B6B,color:#fff
|
||
style G fill:#00D866,color:#fff
|
||
```
|
||
|
||
### 参数化查询(唯一有效的手段)
|
||
|
||
```go
|
||
// ❌ 危险拼接(SQL 注入漏洞)
|
||
query := fmt.Sprintf("SELECT * FROM users WHERE username = '%s'", userInput)
|
||
db.Query(query)
|
||
|
||
// ✅ 正确:使用占位符(Driver 层处理转义)
|
||
db.Query("SELECT * FROM users WHERE username = ?", userInput)
|
||
|
||
// ✅ 正确:GORM Prepared Statement(预编译,性能更好)
|
||
db.Where("username = ?", userInput).First(&user)
|
||
```
|
||
|
||
```java
|
||
// Java PreparedStatement(预编译,同一语句可多次执行不同参数)
|
||
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
|
||
ps.setInt(1, userId);
|
||
ResultSet rs = ps.executeQuery();
|
||
```
|
||
|
||
### 不能参数化的场景及替代方案
|
||
|
||
> [!IMPORTANT] 参数化只能处理**数据值**,不能处理标识符(表名、列名、排序方向)。
|
||
|
||
```sql
|
||
-- 场景:动态表名(LIKE 'prefix_%')
|
||
-- ❌ 不能用参数化,因为参数不能用于标识符
|
||
SELECT * FROM ? WHERE id = 1; -- 语法错误
|
||
|
||
-- ✅ 白名单校验 + 反引号包裹
|
||
allowedTables := map[string]bool{
|
||
"users": true, "orders": true, "products": true,
|
||
}
|
||
if !allowedTables[tableName] {
|
||
return errors.New("invalid table name")
|
||
}
|
||
query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ?", tableName)
|
||
db.Query(query, id)
|
||
|
||
-- ✅ 动态 ORDER BY 使用白名单
|
||
allowedColumns := map[string]bool{"created_at": true, "name": true, "id": true}
|
||
col := r.URL.Query().Get("order_by")
|
||
if !allowedColumns[col] {
|
||
col = "created_at" // 默认排序
|
||
}
|
||
// 再配合 ASC/DESC 白名单
|
||
direction := "ASC"
|
||
if dir := r.URL.Query().Get("direction"); dir == "DESC" {
|
||
direction = dir
|
||
}
|
||
query := fmt.Sprintf("SELECT * FROM users ORDER BY `%s` %s", col, direction)
|
||
```
|
||
|
||
## 传输加密
|
||
|
||
### SSL/TLS 配置
|
||
|
||
> [!TIP] 为什么需要 TLS?
|
||
> 内网通信不等于安全。同一台交换机下可以用 tcpdump 抓包,中间人攻击(MITM)在容器化环境中也完全可行。
|
||
|
||
```bash
|
||
# 生成 CA + 服务端证书
|
||
mysql_ssl_rsa_setup --datadir=/var/lib/mysql/mysql-ssl
|
||
|
||
# my.cnf 配置服务端 SSL
|
||
[mysqld]
|
||
require_secure_transport = ON # 强制所有连接使用 SSL(MySQL 8.0.12+)
|
||
ssl-ca = /var/lib/mysql/mysql-ssl/ca.pem
|
||
ssl-cert = /var/lib/mysql/mysql-ssl/server-cert.pem
|
||
ssl-key = /var/lib/mysql/mysql-ssl/server-key.pem
|
||
|
||
# 可选:禁止不安全的 LOCAL_INFILE(防止通过 LOAD DATA LOCAL 读客户端文件)
|
||
local-infile = 0
|
||
```
|
||
|
||
```go
|
||
// Go 驱动 SSL 配置(生产环境必须验证证书)
|
||
import (
|
||
"crypto/tls"
|
||
_ "github.com/go-sql-driver/mysql"
|
||
)
|
||
|
||
tlsConfig := &tls.Config{
|
||
MinVersion: tls.VersionTLS12, // 不支持 TLS 1.0/1.1
|
||
InsecureSkipVerify: false, // ⚠️ 测试环境可用 true,生产必须 false
|
||
ClientAuth: tls.RequireAndVerifyClientCert, // mTLS:双向认证(可选)
|
||
}
|
||
mysql.RegisterTLSConfig("custom", tlsConfig)
|
||
|
||
dsn := "user:pass@tcp(host:3306)/db?tls=custom&parseTime=True"
|
||
db, _ := sql.Open("mysql", dsn)
|
||
```
|
||
|
||
> [!CHECK] 验证 TLS 是否生效
|
||
> ```sql
|
||
> SHOW SESSION STATUS LIKE 'Ssl_cipher';
|
||
> -- 如果返回空,说明当前连接未使用加密
|
||
>
|
||
> SHOW VARIABLES LIKE '%ssl%';
|
||
> -- Require_SSL 应为 YES
|
||
> ```
|
||
|
||
### 身份认证插件
|
||
|
||
> [!NOTE] caching_sha2_password vs mysql_native_password
|
||
> MySQL 8.0 默认使用 `caching_sha2_password`,它支持 SHA-256 哈希和密码缓存机制(性能更好)。但部分老旧驱动(如 Python MySQLdb < 2.1.0)只支持 `mysql_native_password`。
|
||
|
||
```sql
|
||
-- MySQL 8.0 默认使用 caching_sha2_password(更安全)
|
||
-- legacy 客户端可能需要切换回 mysql_native_password
|
||
ALTER USER 'app_user'@'%' IDENTIFIED WITH mysql_native_password BY 'password';
|
||
|
||
-- 检查当前使用的认证插件
|
||
SELECT user, host, plugin, authentication_string
|
||
FROM mysql.user;
|
||
|
||
-- 统一切换为更安全的方式(推荐)
|
||
ALTER USER 'app_user'@'%' IDENTIFIED WITH caching_sha2_password BY 'new_strong_password';
|
||
```
|
||
|
||
## 信息与版本泄露
|
||
|
||
> [!QUESTION] 你在帮敌人认识你?
|
||
> 数据库版本号让攻击者知道该用哪些 CVE 漏洞;主机名可能暴露部署架构。
|
||
|
||
```sql
|
||
-- ❌ 默认暴露了大量信息
|
||
SELECT VERSION(), @@hostname, @@basedir;
|
||
|
||
-- 方法1: 使用反向代理隐藏真实端口
|
||
-- nginx 配置:将 /db 路径转发到 MySQL,应用通过 HTTP 走 ProxyProtocol
|
||
|
||
-- 方法2: 自定义 error_log 输出格式
|
||
[mysqld]
|
||
log_error_verbosity = 2 # 减少详细错误信息输出
|
||
```
|
||
|
||
## 审计日志
|
||
|
||
> [!IMPORTANT] 通用日志 vs 专用审计
|
||
> `general_log` 记录一切(包括普通查询),性能开销巨大。**仅用于临时排查**,不适合长期开启。生产审计推荐使用专用审计插件。
|
||
|
||
```sql
|
||
-- MySQL Enterprise Audit Plugin(商业版)
|
||
-- 社区版可以用 general_log 作为短期替代方案
|
||
|
||
-- ⚠️ 开启通用日志(性能开销较大,仅用于临时审计)
|
||
SET GLOBAL general_log = 'ON';
|
||
SET GLOBAL general_log_file = '/var/log/mysql/general.log';
|
||
SET GLOBAL general_log = 'OFF'; -- 查完后立即关闭
|
||
|
||
-- slow_query_log 长期开启(记录慢查询,便于发现异常高频查询)
|
||
SET GLOBAL slow_query_log = 'ON';
|
||
SET GLOBAL long_query_time = 2; -- 超过 2 秒的记录
|
||
SET GLOBAL log_queries_not_using_indexes = 'ON';
|
||
```
|
||
|
||
### 社区审计方案(DDL/DML 追踪)
|
||
|
||
```sql
|
||
-- 记录所有 DDL 操作到自定义审计表
|
||
CREATE TABLE ddl_audit (
|
||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||
event_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
user_host VARCHAR(255),
|
||
ddl_statement TEXT
|
||
);
|
||
|
||
-- DML 行级变更追踪(触发器方式)
|
||
DELIMITER //
|
||
CREATE TRIGGER audit_users_insert_after
|
||
AFTER INSERT ON users
|
||
FOR EACH ROW
|
||
BEGIN
|
||
INSERT INTO dml_audit (action, table_name, row_id, changed_at)
|
||
VALUES ('INSERT', 'users', NEW.id, NOW());
|
||
END//
|
||
|
||
CREATE TRIGGER audit_users_update_after
|
||
AFTER UPDATE ON users
|
||
FOR EACH ROW
|
||
BEGIN
|
||
IF OLD.status != NEW.status THEN
|
||
INSERT INTO dml_audit (action, table_name, row_id, old_val, new_val, changed_at)
|
||
VALUES ('UPDATE', 'users', NEW.id, OLD.status, NEW.status, NOW());
|
||
END IF;
|
||
END//
|
||
DELIMITER ;
|
||
|
||
-- 使用 EVENT TRIGGER(MySQL 8.0.16+)记录 DDL
|
||
CREATE EVENT TRIGGER trg_ddl_audit
|
||
ON OBJECT SCALAR
|
||
EVENT DDL
|
||
EXECUTE AS OWNER
|
||
WHEN (CURRENT_USER() NOT IN ('root'))
|
||
DO LOG 'DDL detected by ' || CURRENT_USER();
|
||
```
|
||
|
||
> [!TIP] 开源审计替代方案
|
||
> - **MariaDB Audit Plugin**: 免费,支持事件类型过滤
|
||
> - **Percona Audit Log Plugin**: Percona Server 内置
|
||
> - **MySQL Router + ProxySQL**: 通过网络代理层拦截和记录所有查询
|
||
|
||
## 安全加固 Checklist
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
A["🔒 MySQL 安全加固"] --> B["1. 访问控制"]
|
||
B --> B1["最小权限原则"]
|
||
B --> B2["限制来源 IP/CIDR"]
|
||
B --> B3["禁用匿名账户"]
|
||
|
||
A --> C["2. 密码安全"]
|
||
C --> C1["validate_password 插件"]
|
||
C --> C2["定期轮换密码"]
|
||
C --> C3["禁用空密码账户"]
|
||
|
||
A --> D["3. 传输加密"]
|
||
D --> D1["SSL/TLS 强制"]
|
||
D --> D2["TLS 1.2+"]
|
||
D --> D3["禁用 LOAD DATA LOCAL"]
|
||
|
||
A --> E["4. 审计日志"]
|
||
E --> E1["slow_query_log 常开"]
|
||
E --> E2["general_log 按需开关"]
|
||
E --> E3["binlog 备份保护"]
|
||
|
||
A --> F["5. 信息管控"]
|
||
F --> F1["隐藏版本/主机名"]
|
||
F --> F2["custom error messages"]
|
||
F --> F3["关闭 performance_schema 敏感暴露"]
|
||
|
||
A --> G["6. OS & 网络"]
|
||
G --> G1["mysql 目录 chmod 700"]
|
||
G --> G2["删除 test 库 + 示例数据"]
|
||
G --> G3["防火墙限制 3306 端口"]
|
||
|
||
style B1 fill:#00D866,color:#fff
|
||
style D1 fill:#00B6BC,color:#fff
|
||
style F1 fill:#FFB800,color:#fff
|
||
```
|
||
|
||
## 关联笔记
|
||
|
||
- [[hhs/GORM/01-安装与初始化]] — GORM 连接参数中的安全配置
|
||
- [[hhs/DEV/Go-Database]] — Go 驱动的安全连接配置
|