Files
cs-note/hhs/MySQL/08-工程实践/39-安全加固.md
T
2026-05-24 11:42:38 +08:00

363 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 驱动的安全连接配置