This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/MySQL/39-安全加固.md
T
2026-05-17 00:06:11 +08:00

12 KiB
Raw Blame History

tags, create time
tags create time
MySQL
安全加固
SSL/TLS
SQL Injection
ACL
2026-05-16 00:00

安全加固

概述

数据库安全涉及访问控制、传输加密、数据保护和防御攻击等多个层面。MySQL 内置了丰富的安全机制,但默认配置往往不够严格。本节梳理生产环境必须做的安全加固措施。

最小权限原则

[!QUESTION] 思考 为什么应用账号不应该拥有 DROP 或 ALTER 权限?如果业务确实需要建表,应该怎么设计?

-- ❌ 最差实践:应用直接用 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 直接授权 当用户数量增多时,逐个给用户赋权难以维护。角色可以理解为"权限模板",给角色授权后再把角色分配给用户。

-- 创建角色(权限模板)
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 初始化后会默认创建一些不安全的账户和数据库,很多团队忽略了这一步。

-- 初始化后必须执行的操作:
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 这类弱口令是黑客的首选攻击目标。

-- 启用密码强度验证插件
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 代码来执行。参数化查询通过驱动层发送参数,服务端将其视为纯数据而非可执行语句。

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

参数化查询(唯一有效的手段)

// ❌ 危险拼接(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 PreparedStatement(预编译,同一语句可多次执行不同参数)
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
ps.setInt(1, userId);
ResultSet rs = ps.executeQuery();

不能参数化的场景及替代方案

[!IMPORTANT] 参数化只能处理数据值,不能处理标识符(表名、列名、排序方向)。

-- 场景:动态表名(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)在容器化环境中也完全可行。

# 生成 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 驱动 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 是否生效

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。

-- 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 漏洞;主机名可能暴露部署架构。

-- ❌ 默认暴露了大量信息
SELECT VERSION(), @@hostname, @@basedir;

-- 方法1: 使用反向代理隐藏真实端口
-- nginx 配置:将 /db 路径转发到 MySQL,应用通过 HTTP 走 ProxyProtocol

-- 方法2: 自定义 error_log 输出格式
[mysqld]
log_error_verbosity = 2  # 减少详细错误信息输出

审计日志

[!IMPORTANT] 通用日志 vs 专用审计 general_log 记录一切(包括普通查询),性能开销巨大。仅用于临时排查,不适合长期开启。生产审计推荐使用专用审计插件。

-- 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 追踪)

-- 记录所有 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

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

关联笔记