摘要
审计是数据库安全中很重要的一个环节,它能够实时记录数据库的操作记录,帮助数据库管理员对数据库异常行为进行分析审核。目前除了MySQL商业版的审计插件外,还有三种开源的审计插件可以考虑:Percona的审计插件audit log;MariaDB的审计插件server audit;Mcafee的审计插件libaudit_plugin。目前InnoSQL 5.7版本已经加入audit_log审计功能,本文主要对audit_log审计插件的框架和审计日志记录流程进行代码分析。
背景
审计是数据库安全中很重要的一个环节,它能够实时记录数据库的操作记录,帮助数据库管理员对数据库异常行为进行分析审核。使用审计功能的优点:对每一时刻每一用户的操作都有记录。比如某一个业务在某个时间点出现了异常,因为异常操作(比如DDL)导致系统出现了严重的问题,这个时候如果要查看这个问题的具体情况,谁登陆了系统,什么时候登陆的,做了什么操作等等,就可以通过审计log进行调查。当然审计的缺点也很明显,审计日志信息比较大,记录审计日志对Sever的性能有一定的影响。
目前MySQL社区版是没有审计功能的,从5.5开始只在商业版中有,需要单独收取licence费用。除了MySQL商业版的审计插件外,还有三种开源的审计插件可以考虑:
- Percona的审计插件audit log:
此插件是Percona 的内置审计插件,兼容性不高,需要适配。
- MariaDB的审计插件server audit:
此插件是MariaDB的内置审计插件,MariaDB_5.5.37版本和MariaDB_10.0.10以后版本的audit插件支持MariaDB、 MySQL和Percona Server使用。
- Mcafee的审计插件libaudit_plugin:
此插件已经在github上开源,它是McAfee公司基于percona开发的MySQL审计插件。McAfee的MySQL Audit插件以JSON格式保存,日志信息比较大。
目前已经将开源版本Percona-5.7.26-29的audit_log插件适配到InnoSQL-5.7.20版本,基本功能已经测试通过,并对审计性能进行了优化。下面主要对audit_log审计插件的框架和审计日志记录流程进行代码分析。
审计代码分析
从MySQL5.5开始,内核中已经增加了一套server层的审计接口,添加了额外的审计埋点对我们所关心的地方进行事件捕获,并将捕获的事件传给审计插件进行处理。
审计插件对支持的事件按照设定的过滤规则和记录格式进行处理后,根据设定的日志记录方式,将审计日志记录到文件或系统日志中。
图2-1 审计流程概况
Server审计入口
下面是mysql服务器层审计机制中调用审计入口函数mysql_audit_notify()的地方,但audit_log插件并不支持所有的事件。其中只有红色部分的事件被audit_log支持,包括connect/disconnect连接断开,query语句,change_user命令,以及触发器、存储过程和事件中SQL语句。另外audit_log插件会在安装和卸载插件时,记录Audit/NoAudit审计日志。
图2-2 Sever审计入口
在plugin_audit.h中,server层提供了11种审计事件,
但是从审计插件声明和进入插件入口函数中的判断来看,插件注册了三种事件,但根据设置的审计策略不同,也只会处理GENERAL和CONNECT两种事件。
审计插件声明
审计插件的声明注册了审计事件进入插件的入口函数audit_log_notify,审计插件支持的事件类型,以及审计插件初始化和释放函数,审计插件状态变量和系统变量。其中每种事件类型有不同的子事件类型,只有红色事件对应的审计日志才会被处理记录,这里也对应着2.1中的审计入口说明。
- MYSQL_AUDIT_GENERAL_ALL
- MYSQL_AUDIT_GENERAL_LOG
- MYSQL_AUDIT_GENERAL_ERROR
- MYSQL_AUDIT_GENERAL_RESULT
- MYSQL_AUDIT_GENERAL_STATUS
- MYSQL_AUDIT_CONNECTION_ALL
- MYSQL_AUDIT_CONNECTION_CONNECT
- MYSQL_AUDIT_CONNECTION_DISCONNECT
- MYSQL_AUDIT_CONNECTION_CHANGE_USER
- MYSQL_AUDIT_CONNECTION_PRE_AUTHENTICATE
- MYSQL_AUDIT_TABLE_ACCESS_ALL
- MYSQL_AUDIT_TABLE_ACCESS_READ
- MYSQL_AUDIT_TABLE_ACCESS_INSERT
- MYSQL_AUDIT_TABLE_ACCESS_UPDATE
- MYSQL_AUDIT_TABLE_ACCESS_DELETE
图2-3-1 审计插件声明
安装审计初始化函数主要是初始化系统变量、锁以及对系统变量合理性进行检查,并将过滤变量用户名,命令,数据库名称的值(逗号分隔)解析出来并插入哈希表。然后根据系统变量audit_log_handler设置的日志存储方式(FILE/SYSLOG)调用init_new_log_file()进行相应的初始化,如果是FILE格式并且使用buffer缓存(审计策略audit_log_strategy为ASYNCHRONOUS或者PERFORMANCE),则创建刷新线程audit_log_flush_worker。然后打开审计日志文件。如果是SYSLOG格式,则调用openlog打开系统日志。
图2-3-2 审计插件安装初始化函数
卸载审计插件会调用deinit函数,先记录卸载日志,然后再关闭日志文件并释放空间。
图2-3-3 审计插件卸载函数
审计日志记录流程
Sever层在需要进行审计的地方调用审计入口函数mysql_audit_notify(),首先检查插件是否支持该事件类型,如果不支持则return不进入审计日志的记录流程。如果支持该事件,则从thd中获取query和字符集信息,然后将事件发给审计插件,调用audit_log_notify()进入审计日志记录流程。
图2-4-1 Server审计入口
审计入口函数audit_log_notify(),根据传入事件的信息判断是否满足用户、库名、SQL命令的过滤条件,再判断审计策略audit_log_policy是否满足ALL、NONE、LOGINS或者QUERIES中的一种,如果是MYSQL_AUDIT_GENERAL_CLASS事件类型并且是MYSQL_AUDIT_GENERAL_STATUS子事件,则调用audit_log_general_record()按照审计日志的格式获取日志信息,并调用audit_log_write()记录审计日志。如果是MYSQL_AUDIT_CONNECTION_CLASS事件,并且是子事件MYSQL_AUDIT_CONNECTION_CONNECT、MYSQL_AUDIT_CONNECTION_DISCONNECT或者MYSQL_AUDIT_CONNECTION_CHANGE_USER中的一种,则调用audit_log_connection_record()按照审计日志的格式获取日志信息,并调用audit_log_write()记录审计日志。
Audit_log_write()调用handler->write()写入日志,如果日志是记录到审计日志文件中,则调用audit_handler_file_write(),判断如果使用buffer即audit_log_strategy= ASYNCHRONOUS或 PERFORMANCE,则调用audit_handler_file_write_buf()将日志写入buffer缓冲;如果不使用buffer,即audit_log_strategy= SYNCHRONOUS或SEMISYNCHRONOUS,则调用audit_handler_file_write_nobuf()将日志直接写入文件缓存,接着判断如果audit_log_strategy= SYNCHRONOUS,则调用logger_sync()将文件缓存同步刷入磁盘。如果是记录到系统日志SYSLOG中,则调用audit_handler_syslog_write()->syslog()将审计日志记录到系统日志。
图2-4-2 审计插件内部入口流程
对于使用buffer方式记录审计日志的情况,会根据日志大小进行不同的方式写入。如果日志长度大于buffer大小,并且日志记录策略不是PERFORMANCE(缓冲区满时删消息),则会调用audit_handler_file_write_buf()将日志写入文件缓冲区。如果日志写入位置加上写入日志长度小于等于文件刷新位置加上buffer大小,则将日志信息拷贝至日志buffer,否则如果日志记录策略不是PERFORMANCE,则等待日志刷新线程完成日志刷新后继续loop判断将日志写入buffer。如果日志写入的大小超过buffer大小的一半时,则立即唤醒刷新线程将buffer中的日志写入文件缓冲区。
图2-4-3 使用buffer的审计日志写入
如果不使用buffer,则调用audit_handler_file_write_nobuf()将日志内容直接写入文件缓冲区,再判断如果刷新线程刷完buffer日志,并且设置audit_log_rotations大于0,即使用日志文件循环记录,并且日志文件大小没有达到ulonglong-1,但日志文件大小超过了日志文件循环的阈值audit_log_rotate_on_size,则调用do_rotate()进行日志文件循环。
图2-4-3 不使用buffer的审计日志写入
日志刷新线程
前面提到,如果是将审计日志记录文件中,并且使用buffer的方式记录,那么会创建一个刷新线程将buffer中的日志写入文件缓冲区。这里介绍一下刷新线程的工作流程。
audit_log_flush_worker()循环判断,如果日志文件没有关闭;或者文件关闭,但是buffer中仍有未刷入文件的日志,即log->flush_pos 不等于 log->write_pos,则调用audit_log_flush()进行刷新。
刷新函数audit_log_flush()中循环判断如果buffer中没有新写入的审计日志,即log->flush_pos等于 log->write_pos,并且日志文件没有关闭,则等待1s循环或者等待被唤醒,即当buffer日志写入大小超过一半时会唤醒刷新等待。如果日志文件关闭,则return返回。
如果buffer中有新的审计日志写入,并且判断如果log->flush_pos 大于等于 log->write_pos % log->size,即log->write_pos大于等于buffer size,则将log->size - log->flush_pos大小的日志写入日志文件缓冲,此时buffer中的日志并未写完,还有log->write_pos-log->size大小等待写入,状态标志为LOG_RECORD_INCOMPLETE,并将log->flush_pos置0,log->write_pos对buffer size取模,再将写入文件缓冲区的日志大小累计至audit_log_flushlen。如果log->flush_pos 小于 log->write_pos % log->size,则将log-> write_pos - log->flush_pos大小的日志写入文件缓冲,buffer日志写完,状态标志为LOG_RECORD_INCOMPLETE,并将写入文件缓冲区的日志大小累计至audit_log_flushlen。
如果audit_log_fsync_size不为0,并且文件缓冲区的日志量大于等于设置的阈值audit_log_fsync_size,则调用fsync将日志文件缓冲区的日志刷入磁盘,刷盘后将audit_log_flushlen置0,并广播刷新完成信号,唤醒buffer写入等待。
图2-5 刷新线程处理流程
审计日志格式说明
默认OLD格式,日志格式可以通过设置audit_log_format为OLD(XML格式)、NEW(NEW要比OLD的标签详细一些)、JSON和CSV。
审计日志提供了三种事件监控记录:audit、query 和 connection。每种事件都有不同的字段记录审计日志信息(红色字段为InnoSQL新增字段):
AUDIT事件字段说明:
- NAME:事件名称,Audit表示log开始,NoAudit表示log结束
- RECORD:唯一的记录ID
- TIMESTAMP:时间戳
- MYSQL_VERSION:server版本号
- STARTUP_OPTIONS:命令行启动参数
- OS_VERSION:操作系统版本号
QUERY事件字段说明:
- NAME:事件名称,如Query, Prepare, Execute, Change user等
- RECORD:唯一的记录ID
- TIMESTAMP:时间戳
- COMMAND_CLASS: 操作命令类,如select, alter_table, create_table等
- CONNECTION_ID:连接ID
- STATUS:非0表示有error
- SQLTEXT:SQL语句
- DIGEST_TEXT:记录SQL模板
- QUERY_TIME:记录SQL执行时间
- LOCK_TIME:记录SQL加锁时间
- SCAN_ROWS:记录SQL扫描行数
- AFFECTED_ROWS:记录SQL更新行数
- USER:连接用户名
- HOST:连接主机名
- OS_USER:外部用户名
- IP:连接用户IP
- DB:连接时指定的数据库名
CONNECTION事件字段说明:
-
- NAME:事件名称,如Connect, Quit
- RECORD:唯一的记录ID
- TIMESTAMP:时间戳
- CONNECTION_ID:连接ID
- STATUS:0表示连接成功,非0表示连接失败
- USER:连接用户名
- PRIV_USER:经过身份验证的用户名
- OS_LOGIN:外部用户名
- PROXY_USER:代理用户名
- HOST:连接主机名
- IP:连接用户IP
- DB:连接时指定的数据库名
审计系统变量说明
注:红色为InnoSQL新增变量
性能优化
在Percona开源audit_log版本中存在性能周期性波动的问题,在InnoSQL的audit_log版本中进行了优化:
- Percona性能问题:刷新线程只将内存buffer中的审计日志写入文件缓冲区,并未主动调用fsync()将文件缓冲刷入磁盘,而是让操作系统内核自己fsync,操作系统调用pdflush线程定期/proc/sys/vm/dirty_expire_centisecs(单位是1/100秒,默认3000,即30s)刷盘,这会导致sysbench在8核16GB配置的云主机测试20G数据32并发oltp_read_write时会出现TPS 30s左右周期性剧烈波动。
- 优化方案:增加文件缓冲大小的阈值系统变量audit_log_fsync_size,在异步刷新线程audit_log_flush_worker()->audit_log_flush()中累计已经写入文件缓冲中的日志大小audit_log_flushlen,如果阈值设置不为0,并且audit_log_flushlen超过文件缓冲阈值大小audit_log_fsync_size,则进行fsync,同时累计文件缓冲大小audit_log_flushlen置0。
下面是优化前后与未开启审计的性能对比:
表5-1 TPS性能优化对比
图5-1 性能优化曲线对比图
总结
- 从代码分析、功能和性能测试结果来看,目前audit_log审计插件可以满足基本数据库审计需求。
- 从审计插件目前已支持的事件类型和Server层的审计入口来看,可以根据业务需求丰富审计日志的记录种类。
- 业务开启审计功能之后,审计日志文件的大小会快速增长,需要额外空间存放日志文件。