在数据库运维体系中,DBA往往拥有最高权限,其直接执行的每一条SQL都可能影响核心业务。要做到真正可追溯,必须实现指令级审计,也就是把谁、在什么时间、从哪台机器、执行了什么语句全部记录下来,并且把这些数据集中到第三方堡垒机里统一管理和审计。

为什么原生日志无法满足DBA指令级审计
MySQL自带的通用查询日志(general log)虽然能记录所有SQL,但它是全局开关,开启后磁盘IO压力极大,而且日志里只显示连接的线程ID,无法直接对应到真实DBA账号。慢查询日志又只捕获超过阈值的语句,大量普通高危操作会被漏掉。
此外,DBA通常通过跳板机或本地客户端使用同一个数据库账号(如root)连入,从数据库视角看全是同一个用户,无法区分实际操作人。这就需要我们在MySQL之外建立账号映射,并通过堡垒机把真实身份与数据库会话绑定。
基于init-connect的会话级身份绑定
init-connect是MySQL的一个系统变量,它定义每个普通用户连接建立时自动执行的SQL。我们可以利用它,在连接时把当前连接的线程ID、登录系统用户、客户端IP写入一张审计表,从而建立线程与真实DBA的映射。
需要注意,init-connect对具有SUPER权限的用户不生效,因此DBA不能使用root直连,而应分配普通权限账号并授予必要操作权,从机制上强制分权。以下为建表与参数配置示例:
-- 创建审计库与映射表 CREATE DATABASE IF NOT EXISTS audit_db; USE audit_db; CREATE TABLE conn_map ( id BIGINT AUTO_INCREMENT PRIMARY KEY, thread_id BIGINT, os_user VARCHAR(50), db_user VARCHAR(50), client_ip VARCHAR(50), connect_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 设置init-connect,要求每个连接先插入映射 SET GLOBAL init_connect = 'INSERT INTO audit_db.conn_map(thread_id,os_user,db_user,client_ip) VALUES (CONNECTION_ID(),CURRENT_USER(),USER(),SUBSTRING_INDEX(USER(),"@",-1))';
上述方式成本低、对业务无侵入,但仅能记录连接事件。要拿到具体执行的SQL,还需配合审计插件或binlog解析。
通过审计插件捕获指令级SQL
社区版MySQL可借助如MariaDB Audit Plugin或第三方审计插件,将每条SQL事件输出到本地文件或syslog。企业版则自带Audit Log插件。我们可配置插件把事件格式化为JSON,包含用户名、主机、SQL文本、执行结果。
下面以MariaDB审计插件配置为例,说明如何输出到文件并控制字段:
[mysqld] plugin_load = server_audit.so server_audit_logging = ON server_audit_events = QUERY,QUERY_DDL,QUERY_DML server_audit_file_path = /var/log/mysql/audit.log server_audit_file_rotate_size = 1000000 server_audit_excl_users = monitor_user
日志落盘后,由采集agent读取并转发。这里要注意排除监控账号,避免噪声。同时SQL中可能含敏感数据,传输前应在agent侧做脱敏。
与第三方堡垒机系统的集成方式
堡垒机通常提供Syslog接收或HTTP API接入。我们采用agent中转模式:agent部署在MySQL所在主机,实时跟踪审计日志,按堡垒机要求的协议封装后推送。这样数据库本身不需改动网络出口。
若堡垒机支持数据库协议代理,也可让DBA统一连堡垒机,由堡垒机代连MySQL,此时指令级审计由堡垒机直接抓取代理流量完成,MySQL侧只需保留基础日志备查。两种模式对比如下:
| 集成模式 | 改造量 | 审计精度 | 适用场景 |
|---|---|---|---|
| 日志转发集成 | 低,装agent即可 | 高,含执行结果 | 已有MySQL不便改架构 |
| 堡垒机代理直连 | 高,DBA须改连接习惯 | 最高,含完整会话回放 | 新建运维体系 |
实践中,多数团队选日志转发,因为DBA操作习惯不变,推广阻力小。agent可用Python或Go编写,通过tail文件或监听syslog实现。
基于Go的轻量转发示例
下面示例展示如何用Go读取审计日志行,并调用堡垒机HTTP接口上报。此处假设堡垒机地址为https://audit.ipipp.com/api/v1/sql,实际请替换为你的环境地址。
package main
import (
"bufio"
"fmt"
"net/http"
"os"
"strings"
"time"
)
func main() {
f, err := os.Open("/var/log/mysql/audit.log")
if err != nil {
fmt.Println("open log fail")
return
}
defer f.Close()
reader := bufio.NewReader(f)
client := &http.Client{Timeout: 5 * time.Second}
for {
line, err := reader.ReadString('n')
if err != nil {
time.Sleep(2 * time.Second)
continue
}
if strings.Contains(line, "QUERY") {
// 简化示例:直接POST原始行
resp, postErr := client.Post("https://audit.ipipp.com/api/v1/sql", "text/plain", strings.NewReader(line))
if postErr == nil {
resp.Body.Close()
}
}
}
}
该代码仅为骨架,生产环境需增加断点续传、批量发送与失败重试。关键是把审计事件与conn_map中的线程ID关联,还原出真实DBA身份再上报堡垒机。
性能与合规平衡建议
指令级审计不可避免带来开销。建议只对DBA账号及相关高危库开启插件审计,而非全实例。同时把审计表与日志放在独立磁盘,避免影响数据盘IO。
在合规层面,审计数据自身也需保护,堡垒机应设权限分离,审计员不能删日志,DBA不能改审计配置。定期导出审计数据归档,满足等保要求的留存周期。
常见误区与规避
有人认为开了general log就算审计,其实它既无身份绑定也无集中存储,且性能损耗大,不能作为正式方案。还有团队让DBA继续用root,仅靠堡垒机录屏,这样无法精确检索某条SQL,出事时定位慢。
正确做法是数据库侧做最小权限与身份映射,堡垒机侧做集中存储与检索,两者互补。只有把指令级数据结构化送入堡垒机,才能在故障或违规时秒级定位责任人。