摘要:RDS for MySQL的审计日志功能在用户活动监控、权限变更追踪和性能优化等方面有着重要的作用。
本文分享自华为云社区《【华为云MySQL技术专栏】RDS for MySQL 审计日志功能介绍》,作者:GaussDB数据库。
1. 背景
在生产环境中,当数据库出现故障或问题时,运维人员需要快速定位出异常或者高危的SQL语句。这时,审计日志能够提供详细的记录,帮助追踪每个数据库操作的执行者、执行时间以及受影响的数据对象,从而大大加速故障排查和恢复流程。
MySQL企业版提供了审计日志插件,可以对数据库操作进行细粒度的审计。该插件支持记录用户登录、查询执行、数据修改等重要操作。然而,在MySQL社区版中,只是提供了审计日志的相关插件接口定义和功能描述,并不支持原生的审计日志功能。
为了弥补这一功能的缺失,华为云RDS for MySQL通过集成Percona公司开源的审计日志插件,实现了MySQL审计日志功能。该功能已在RDS for MySQL 5.7和RDS for MySQL 8.0版本中开放,满足了用户对数据库安全审计的需求,同时增强了数据库的合规性和可用性。
本文将以RDS for MySQL为研究对象,对于审计日志进行功能介绍和原理解析。
2. 功能参数介绍
当在RDS for MySQL上开启审计日志功能,用户可以通过SHOW variables LIKE 'audit%';语句查看与审计日志功能相关的变量名和参数值。
mysql> SHOW variables LIKE 'audit%';
+-------------------------------------+---------------+
| Variable_name | Value |
+-------------------------------------+---------------+
| audit_log_anonymized_ip | |
| audit_log_buffer_size | 1048576 |
| audit_log_csv2_escape | OFF |
| audit_log_csv2_old_separated_format | OFF |
| audit_log_csv2_truncation | ON |
| audit_log_exclude_accounts | |
| audit_log_exclude_commands | |
| audit_log_exclude_databases | |
| audit_log_file | audit.log |
| audit_log_flush | OFF |
| audit_log_force_rotate | OFF |
| audit_log_format | CSV2 |
| audit_log_handler | FILE |
| audit_log_include_accounts | |
| audit_log_include_commands | |
| audit_log_include_databases | |
| audit_log_policy | ALL |
| audit_log_rotate_on_size | 104857600 |
| audit_log_rotations | 50 |
| audit_log_strategy | ASYNCHRONOUS |
| audit_log_syslog_facility | LOG_USER |
| audit_log_syslog_ident | percona-audit |
| audit_log_syslog_priority | LOG_INFO |
+-------------------------------------+---------------+
23 rows in set (0.01 sec)
这些参数控制着审计日志插件的整体功能,允许用户灵活调节审计日志的各个方面。通过合理设置和调整这些参数,用户可以精确确定日志的记录范围、记录级别、存储方式等。例如,用户可以通过audit_log_policy决定审计日志的记录级别,也可以通过改变audit_log_strategy调整审计日志的刷新策略。
在 RDS for MySQL中,部分变量因安全合规考虑未开放修改。下表对审计日志功能中相关变量的作用和默认值进行介绍。
审计日志变量名 |
默认值 |
是否只读 |
功能说明 |
audit_log_format |
CSV2 |
是 |
审计日志的格式,RDS for MySQL审计日志目前一共支持OLD, NEW, JSON, CSV和CSV2五种格式。 |
audit_log_flush |
OFF |
否 |
当此变量设置为ON时,日志文件将被关闭并重新打开。这可用于手动日志轮转。 |
audit_log_policy |
ALL |
否 |
指定审计日志记录的事件 ALL - 记录所有事件 LOGINS - 只记录登录连接信息 QUERIES – 记录所有SQL语句 NONE - 不记录任何事件 |
audit_log_file |
audit.log |
是 |
指定审计日志路径及文件名称,路径可为相对路径或绝对路径 |
audit_log_strategy |
ASYNCHRONOUS |
是 |
指定审计日志的刷新策略,目前支持四种日志刷新策略。 • ASYNCHRONOUS:默认日志刷新策略,使用内存缓冲区进行日志记录,如果缓冲区已满,不会丢弃消息。 • PERFORMANCE:使用内存缓冲区记录日志,如果缓冲区已满则丢弃消息。 • SEMISYNCHRONOUS:直接记录到文件,不用刷新和同步每个事件。 • SYNCHRONOUS:直接将日志记录到文件中,对每个事件进行刷新和同步。 只有在 audit_log_handler 为FILE时该变量才生效。 |
audit_log_buffer_size |
1048576 |
是 |
审计日志缓冲区大小,当audit_log_strategy设置为ASYNCHRONOUS或PERFORMANCE时生效 |
audit_log_rotations |
50 |
是 |
此变量用于指定audit_log_rotate_on_size变量设置为非零值时应保留多少个日志文件。只有当audit_log_handler设置为FILE时,此变量才有效。 |
audit_log_rotate_on_size |
104857600 |
是 |
指定审计日志文件的最大大小。当达到此大小时,审计日志将进行轮转。轮转后的日志文件与当前日志文件位于同一目录中。 |
audit_log_handler |
FILE |
是 |
指定审核日志保存格式:FILE或SYSLOG |
表格 1 审计日志变量介绍
3. 日志内容解析
在RDS for MySQL上,audit_log_policy的默认值为ALL。在该级别下,审计日志会记录包括DML(数据操作语言)、DDL(数据定义语言)、DCL(数据控制语言)操作以及连接或断开连接等数据库活动。需要注意的是,不同类型的活动包含的日志字段有所不同。下面将从常见DDL、DML、DCL操作以及数据库连接与断开连接产生的审计日志进行内容解析。
DDL、DML和DCL
对于DDL、DML和DCL操作,所生成的审计日志格式是相同的。对于日志字段的具体含义详见如下。
-
RECORD ID:审计日志唯一ID,用来标识每条审计日志。
-
STATUS:状态码,非0表示ERROR。
-
NAME:操作命令分类,QUERY、EXECUTE、QUIT、CONNECT等。
-
TIMESTAMP:记录日志发生的时间戳。
-
COMMAND_CLASS:记录DDL、DML和DCL操作的类型,SELECT、INSERT、DELETE等。
-
SQLTEXT:执行SQL语句的内容。
-
USER:连接数据库的用户名。
-
HOST:连接数据库的主机名。
-
IP:连接数据库的客户端IP地址。
-
DATABASE:连接指定的数据库名。
Connect和Disconnect
连接或断开连接事件,在用户登录成功或登录失败时均会有日志记录。与DDL和DDL操作产生的审计日志不同,连接事件产生审计日志增加了PRIV_USER、OS_LOGIN等字段,下面对于连接或断开连接产生的审计日志进行解析。
-
RECORD ID: 审计日志唯一ID,用来标识每条审计日志。
-
STATUS:状态码,非0表示ERROR。
-
NAME: 操作命令分类,QUERY、EXECUTE、QUIT、CONNECT等。
-
TIMESTAMP:记录日志发生的时间戳
-
USER:连接数据库的用户名。
-
PRIV_USER:经过身份验证的用户名。
-
OS_LOGIN:外部用户名。
-
PROXY_USER:代理用户名。
-
HOST:连接数据库的主机名。
-
IP:连接数据库的客户端IP地址。
-
DATABASE:连接指定的数据库名。
通过对审计日志内容的解析,用户不仅可以快速地查看任意时间段数据库的活动状态,还能够准确了解每条SQL语句的详情,包括执行的用户、时间戳、查询类型等关键信息。这样详细的记录为安全审查、问题排查以及性能优化提供了强有力的支撑。
4.RDS for MySQL审计日志原理浅析
RDS for MySQL审计功能的核心是通过不同类型的事件驱动审计日志插件完成对应类型日志的记录。在RDS for MySQL中一共支持两类事件,即一般事件和连接事件。一般事件可以理解为用户执行的DDL、DML和DCL语句。连接事件则是连接数据库(Connect)和断开连接(Disconnect)数据库。审计日志插件支持事件定义的相关代码如下。
static int is_event_class_allowed_by_policy(mysql_event_class_t event_class,num audit_log_policy_t policy) {
static unsigned int class_mask[] = {
/* ALL */
(1 << MYSQL_AUDIT_GENERAL_CLASS) | (1 << MYSQL_AUDIT_CONNECTION_CLASS),
0, /* NONE */
(1 << MYSQL_AUDIT_CONNECTION_CLASS), /* LOGINS */
(1 << MYSQL_AUDIT_GENERAL_CLASS), /* QUERIES */
};
return (class_mask[policy] & (1 << event_class)) != 0;
}
当发生可审计事件时,服务器会调用相关的审计接口,以便向已注册的审计日志插件传递该事件的信息,确保审计插件在必要时能够接收到并处理该事件。
审计日志功能在RDS for MySQL内核的入口函数是mysql_audit_notify。通过对应事件驱动审计日志插件的工作。主要工作流程调用栈如下所示。
do_command
->dispatch_commnad
->mysql_audit_notify
->event_class_dispatch
// 检查当前插件是否需要处理此事件
->plugin_dispatch
// 按事件类别下发审计任务
->audit_log_notify
数据库内核在收到一条SQL的执行请求后,首先,会通过do_command函数处理该连接。处理完成后,由dispatch_command函数依据不同SQL类型进行命令分发。之后,审计入口函数mysql_audit_notify会完成审计日志记录前的准备和校验工作。如果校验通过,则后续工作会由已注册的审计日志插件中的其他函数完成。
通过函数调用栈可以看出,审计日志功能与数据库内核之间实现了高度解耦。二者通过预先注册的函数接口进行对接,这种设计提高了未来功能扩展的灵活性。
审计日志插件的校验和资源准备工作由mysql_audit_acquire_plugins函数完成,当该函数完成校验后,即审计日志插件已完成注册并且相关资源也已完成绑定,接下来将由audit_log_notify函数按照事件的类型和相关参数设定完成任务分发。audit_log_notify会调用audit_log_write函数完成审计日志的写入,audit_log_write会依据audit_log_handler变量的值来判断是写入审计日志文件还是系统日志。
若是写入日志文件中,此时还会判断日志的写入策略;如果audit_log_strategy是PERFORMANCE或ASYNCHRONOUS,则会调用audit_handle_file_write_buf函数,将日志内容写入审计日志插件的缓冲区中,否则会调用audit_handle_file_write_nobuf函数,将日志内容直接写入操作系统文件缓存。
若audit_log_handler值为SYSLOG,则意味着审计日志会直接写到系统日志中。那么则会通过调用audit_handler_syslog_write完成日志向系统日志的写入。审计日志内部函数调用流程如图所示。
图1 审计日志插件工作流程图
从图中可以看到,审计日志的落盘方式主要有两种,分别是通过文件系统完成日志落盘和利用审计日志刷盘线程不断地将缓冲区的日志写入磁盘中。审计日志的缓冲区是在审计日志插件初始化时完成相关资源的分配,其结构体如下:
struct audit_log_buffer {
// 缓冲区内容
char *buf;
// 缓冲区大小
size_t size;
// 写日志位置
size_t write_pos;
// 日志落盘位置
size_t flush_pos;
// 日志落盘工作线程
pthread_t flush_worker_thread;
// 缓冲区是否暂停
int stop;
// 缓冲区满是否丢弃该日志
int drop_if_full;
void *write_func_data;
audit_log_write_func write_func;
mysql_mutex_t mutex;
mysql_cond_t flushed_cond;
mysql_cond_t written_cond;
log_record_state_t state;
};
从审计日志缓冲区结构体可以看到,日志缓冲区主要通过日志写入函数和日志落盘线程完成其核心功能。
当日志需要写入缓冲区时,首先会比较日志的长度和缓冲区的大小。如果审计日志的长度大于日志缓冲区的大小,并且缓冲区满且选择不丢弃该日志时,审计日志的落盘线程暂停工作,并会绕过日志的缓冲区,直接写入文件缓冲区中。当审计日志长度小于日志缓冲区大小时,此时会将日志的内容拷贝到文件缓冲区中,并更新缓冲区write_pos等相关参数,等待日志落盘线程的工作。如果当前写入位置超过整个缓冲区大小的一半,则会立刻通知落盘线程,完成审计日志的落盘。
审计日志的落盘工作主要由日志落盘工作线程完成。如果日志缓冲区没有关闭并且缓冲区中还存在日志尚未落盘,则会循环调用日志落盘函数进行日志写入。
static void *audit_log_flush_worker(void *arg) {
audit_log_buffer_t *log = (audit_log_buffer_t *)arg;
// 线程初始化
my_thread_init();
// 如果日志缓冲区没有关闭并且当前还有日志未落盘
while (!(log->stop && log->flush_pos == log->write_pos)) {
// 进行日志的落盘工作
audit_log_flush(log);
}
// 关闭线程
my_thread_end();
return nullptr;
}
对于日志落盘函数,会通过循环判断write_pos和flush_pos是否相等,如果二者相等并且日志缓冲区没有停止工作,此时会等待1秒进入循环;如果二者不相等,说明缓冲区中有新的日志需要落盘。此时,若write_pos大于缓冲区的大小,日志插件会把flush_pos后的所有日志进行落盘,并将当前日志的状态设为LOG_RECORD_INCOMPLETE。若write_pos在缓冲区大小范围内,则会将该条日志完整写入审计日志文件中并将该日志状态设为LOG_RECORD_COMPLETE。在完成日志落盘操作后,与缓冲区相关变量值会同步更新,并为下次日志落盘做好准备。
5.使用说明
1)数据库实例开启审计日志
登录管理控制台,在云数据库RDS for MySQL的“实例管理”页面,单击目标实例名称,进入基本信息页面。在左侧导航栏,点击“SQL审计”,在“SQL审计”右侧点击开启按钮,在弹框中点击“确定”,打开审计日志开关。
图2 设置审计日志功能
2)审计日志的下载
开启审计日志后,数据库的相关活动都会以日志的形式记录在OBS中,用户可以在控制台界面进行审计日志的下载。
图3 控制台界面下载审计日志
6.总结
RDS for MySQL的审计日志功能在用户活动监控、权限变更追踪和性能优化等方面有着重要的作用。它不仅帮助企业提升数据库的整体安全性,满足日益严格的合规性要求,还在故障排查中提供有价值的信息。这一功能能够精确的记录用户的数据库操作,有助于识别潜在的安全威胁,并为性能瓶颈分析和优化提供详实的数据支持。