对于一个仓库,如果要防盗, 常见做法是出入口全装上监控,一旦有问题了,调监控查找异常情况。对数据库来说也类似,数据库也有出入口,对所有连接出入口监控,记录下所有动作,一旦有问题了,查询历史动作,找到关键信息。如果仓库中的东西价值高,损失已经造成,监控只是事后诸葛,所以往往请专业人士在巡逻防盗,防止偷盗发生。同样数据库审计也有更专业的手段,sql阻断,在动作发生前,对动作行为分析,如果判断符合预先设置的高危动作,直接中止执行,防止对数据造成破坏。 审计定义:能够实时记录网络上的数据库活动,对数据库遭受到的风险行为进行告警,对攻击行为进行阻断
应用层审计 在应用系统中直接审计,语句还没往数据库后台发就先做了审计,不影响数据库性能,对底层用的是什么数据库也不关心,但对应用系统压力比较大,并且应用系统需要解析语句,有一定复杂度。
传输层审计 往往抓包解析实现,对上下层都没什么影响,但同样要解析语句,有一定复杂度,并且如果传输层是通过加密通讯,将无法解析。
插件审计 对于开源数据库,通常都有提供插件方式增加功能。审计可以以插件直接嵌在内核上,当然会对数据库性能有一定影响,但同样因为直接嵌在内核,很多一手信息能直接拿到,比方说上面没办法回避的语法解析就不用做,而且还能直接拿更多的运行态信息,能开发功能强大又灵活的审计功能。
内核审计 直接在内核上实现,所有功能都能实现,也能将性能影响降到最低,但是对后台稳定性会有影响,对开发人员要求高,不管是开源还是非开源数据库,都会非常慎重考虑直接在内核上支持审计。
谈到数据库审计,不能不提oracle,oracle 数据库的审计功能有10多年甚至更长的历史了,对数据库sql审计功能影响深远,几乎任何数据库审计都会参考,下面我们简单了解下oracle审计。 Oracle中审计总体上可分为“标准审计”和“细粒度审计”,标准审计包含语句审计,权限审计,对象审计,细粒度审计也称为“基于政策的审计”:
审 计 类 型 | 说 明 |
---|---|
语句审计 | 按照语句类型审计SQL语句.而不论访问何种特定的模式对象。也可以在数据库中指定一个或多个用户,针对特定的语句审计这些用户 |
权限审计 | 审计系统权限,例如GRANT。和语句审计一样,权限审计可以指定一个或多个特定的用户作为审计的目标 |
对象审计 | 审计特定模式对象上运行的特定语句(例如,DEPARTMENTS表上的UPDATE语句)。模式对象审计总是应用于数据库中的所有用户 |
细粒度的审计 | 根据访问对象的内容来审计表访问和权限。使用程序包DBMS_FGA来建立特定表上的策略 |
默认情况下审计是关闭的,因为开启性能影响大,需要设置一些参数打开它,审计记录结果既可以放文件也可以放系统表中。那么怎么配置需要审计的内容呢?oracle提供了一套审计配置语法来做这事,audit/noaudit,审计和取消审计,是互逆的,如下所示:
示例如下:
// 审计u1.t2这个表的update 不成功的语句
audit update on u1.t2 by access whenever not successful;
// 审计u1.t2这个表的update 成功的语句
audit update on u1.t2 by access whenever successful;
运算式:f+s=a 连续运行上面两个配置语句,等价审计全部
// 审计u1.t2表update全部语句,全部包括两种,成功和失败
audit update on u1.t2 by access;
// 不审计u1.t2表update成功的语句
noaudit update on u1.t2 by access whenever successful;
运算式:a-s=f 连续运行上面两个配置语句,等价只审计失败语句
a:all,全部
f:fail,失败
s:success,成功
这就是oracle审计配置语法用法,配置完成后就能审计到相应的语句,它的成功、失败、全部是存在运算关系的。如何查询比较简单,直接查询相关表就行,不再介绍。
前文提到,插件是数据库实现审计功能的常见手段,mysql上也有不少审计插件,比较有名的Macfee插件,官方audit plugin,mariadb audit plugin,Percona audit plugin。 从功能上来说,这几个插件大同小异,只是展示的审计内容和格式略有差异。从性能上来说,除了macfee插件外,其它几个性能相差不多,宣称都是15%左右影响,macfee则可能达到50%或更多。实际上,确定一个测试性能损失的标准场景是需要商榷的,跑非常简单的语句,几十万Qps,性能影响肯定大,审计性能消耗和语句量直接关联,跑OLAP查询,几秒甚至几十秒执行一个语句,审计性能影响可以说没有,所以审计性能损失得看具体场景。 从实现方式来说,这几个插件也差不太多,从thd对象中取到所需要的属性,按一定规则过滤,按某种策略存盘,以json或xml等格式展示审计结果。
从5.5开始,mysql内核中已经增加了一套的对服务器操作的审计机制的接口,添加了额外的审计流程来对我们所关心的地方进行事件捕获,如果要对服务器进行审计,只要基于这些完善一个插件即可。 mysql审计接口官方第一次是提交到代码是2009年底,历史也比较悠久了,修改和增加的文件并不多,几个关键点介绍下。 1. 在plugin.h中增加一个新的宏 MYSQL_AUDIT_PLUGIN 用来标识一类全新的插件类型:AUDIT插件 2. 增加了一个新的头文件:plugin_audit.h,里面有审计相关的关键的结构体及一些宏定义,如下: mysql_event_general结构,query信息结构,对应非登陆相关的动作,如执行一个语句select或一个命令exit。
struct mysql_event_general
{
unsigned int event_subclass; //子事件类型
int general_error_code; //错误码
unsigned long general_thread_id; //线程号
const char *general_user; //用户名
unsigned int general_user_length; //长度
const char *general_command; //命令,如connect,query,shutdown等,error类型存错误,其它为null
unsigned int general_command_length;
const char *general_query; //sql语句
unsigned int general_query_length;
struct charset_info_st *general_charset; //字符集
unsigned long long general_time; //时间戳
unsigned long long general_rows; //行计数器
MYSQL_LEX_STRING general_host; //来自主机,如localhost
MYSQL_LEX_STRING general_sql_command; //操作类型,如select
MYSQL_LEX_STRING general_external_user; // external 用户
MYSQL_LEX_STRING general_ip; //ip地址
};
mysql_event_connection,连接信息结构,对应登陆登出动作
struct mysql_event_connection
{
unsigned int event_subclass;
int status; //错误码
unsigned long thread_id;
const char *user;
unsigned int user_length;
const char *priv_user;
unsigned int priv_user_length;
const char *external_user;
unsigned int external_user_length;
const char *proxy_user;
unsigned int proxy_user_length;
const char *host;
unsigned int host_length;
const char *ip;
unsigned int ip_length;
const char *database; //数据库名
unsigned int database_length;
};
所有动作都可以用这两个结构来描述,但是并不是说就能满足全部需求,比方说,对general动作,我想知道语句是针对哪些表的,执行了多长时间,无法得到,对于connect动作,我想知道是什么时间发生的,无从得知,这肯定是不够的,仍需我们去继续丰富功能点。
几个关键的宏定义:
#define MYSQL_AUDIT_GENERAL_CLASS 0 //general动作
#define MYSQL_AUDIT_GENERAL_LOG 0 //log时,语句execute前
#define MYSQL_AUDIT_GENERAL_ERROR 1 //error时,反馈用户前
#define MYSQL_AUDIT_GENERAL_RESULT 2 //结果集返回后
#define MYSQL_AUDIT_GENERAL_STATUS 3 //审计流程结束状态
#define MYSQL_AUDIT_CONNECTION_CLASS 1 //connect动作
#define MYSQL_AUDIT_CONNECTION_CONNECT 0 //连接
#define MYSQL_AUDIT_CONNECTION_DISCONNECT 1 //断开
#define MYSQL_AUDIT_CONNECTION_CHANGE_USER 2 //切换用户
3. 核心功能实现代码:sql_auditc.h,sql_audit.cc,初始化,回收,以及最重要的取出thd对象有效值等一系列动作均在此完成。 在sql_auditc.h中,定义了两个内联函数和三个宏,功能是获取到thd对象中有用值,如用户名,数据库名,时间,sql内容等所需要信息,以参数的形式传给下一层接口。每个动作,都会从这里获取原始信息,会频繁调用这几个函数,所以使用了宏和内联函数来提高性能。 在sql_audit.cc中,除了审计插件初始化,回收外,最重要的功能获得操作原始信息,在这里实现了函数获得上面sql_auditc.h中通过参数形式传入的有用信息,并存入mysql_event_general和mysql_event_connection结构。 4. mysql其它一些连接、插入、解析、日志等接口也增加了审计入口函数。如mysqld.cc、sql_parse.cc等文件中函数增加了审计入口。
审计执行流程简介 登陆登出流程
#0 audit_null_notify (thd=0x7fdcc7ff6000, event_class=0, event=0x7fdd45440cb0) at /mysql56/plugin/audit_null/audit_null.c:99
#1 0x0000000000683f22 in plugins_dispatch (thd=0x7fdcc7ff6000, event_subtype=<value optimized out>, ap=<value optimized out>) at /mysql56/sql/sql_audit.cc:455
#2 event_class_dispatch (thd=0x7fdcc7ff6000, event_subtype=<value optimized out>, ap=<value optimized out>) at /mysql56/sql/sql_audit.cc:491
#3 general_class_handler(THD *, uint, typedef __va_list_tag __va_list_tag *) (thd=0x7fdcc7ff6000, event_subtype=<value optimized out>, ap=<value optimized out>)
at /mysql56/sql/sql_audit.cc:90
#4 0x00000000006841b1 in mysql_audit_notify (thd=0x7fdcc7ff6000, event_class=0, event_subtype=0) at /mysql56/sql/sql_audit.cc:217
#5 0x000000000063e7cb in mysql_audit_general_log (thd=0x7fdcc7ff6000, cmd=0xc76b64 "Connect", cmdlen=7, query_str=<value optimized out>, query_len=<value optimized out>)
at /mysql56/sql/sql_audit.h:125
#6 0x000000000063ea5a in log_command (thd=<value optimized out>, command=<value optimized out>, format=0xc6d27b "%s@%s on %s") at /mysql56/sql/log.cc:2094
#7 general_log_print (thd=<value optimized out>, command=<value optimized out>, format=0xc6d27b "%s@%s on %s") at /mysql56/sql/log.cc:2122
#8 0x0000000000682030 in acl_authenticate (thd=0x7fdcc7ff6000, com_change_user_pkt_len=<value optimized out>) at /mysql56/sql/sql_acl.cc:11244
#9 0x00000000006a8ad0 in check_connection (thd=0x7fdcc7ff6000) at /mysql56/sql/sql_connect.cc:685
#10 0x00000000006a8d97 in login_connection (thd=0x7fdcc7ff6000) at /mysql56/sql/sql_connect.cc:754
#11 thd_prepare_connection (thd=0x7fdcc7ff6000) at /mysql56/sql/sql_connect.cc:914
#12 0x00000000006a9119 in do_handle_one_connection (thd_arg=<value optimized out>) at /mysql56/sql/sql_connect.cc:983
#13 0x00000000006a91c2 in handle_one_connection (arg=<value optimized out>) at /mysql56/sql/sql_connect.cc:906
#14 0x0000000000939337 in pfs_spawn_thread (arg=0x7fdccb3be840) at /mysql56/storage/perfschema/pfs.cc:1860
#15 0x00007fdd46a249d1 in start_thread () from /lib64/libpthread.so.0
#16 0x00007fdd459a38fd in clone () from /lib64/libc.so.6
登陆如上所示,登陆时在do_handle_one_connection 中循环等待连接,有请求过来时再到准备连接,再到权限认证检查,最在log_command中进入审计接口,准备审计相关动作。
#0 audit_null_notify (thd=0x7fdcc7eb0000, event_class=1, event=0x7fdd46ca0b40) at /mysql56/plugin/audit_null/audit_null.c:99
#1 0x0000000000684402 in plugins_dispatch (thd=0x7fdcc7eb0000, event_subclass=<value optimized out>, ap=<value optimized out>) at /mysql56/sql/sql_audit.cc:455
#2 event_class_dispatch (thd=0x7fdcc7eb0000, event_subclass=<value optimized out>, ap=<value optimized out>) at /mysql56/sql/sql_audit.cc:491
#3 connection_class_handler(THD *, uint, typedef __va_list_tag __va_list_tag *) (thd=0x7fdcc7eb0000, event_subclass=<value optimized out>, ap=<value optimized out>)
at /mysql56/sql/sql_audit.cc:114
#4 0x00000000006841b1 in mysql_audit_notify (thd=0x7fdcc7eb0000, event_class=1, event_subtype=1) at /mysql56/sql/sql_audit.cc:217
#5 0x000000000057bb53 in close_connection (thd=0x7fdcc7eb0000, sql_errno=0) at /mysql56/sql/mysqld.cc:2634
#6 0x00000000006a90b2 in do_handle_one_connection (thd_arg=<value optimized out>) at /mysql56/sql/sql_connect.cc:996
#7 0x00000000006a91c2 in handle_one_connection (arg=<value optimized out>) at /mysql56/sql/sql_connect.cc:906
#8 0x0000000000939337 in pfs_spawn_thread (arg=0x7fdccb3be7a0) at /mysql56/storage/perfschema/pfs.cc:1860
#9 0x00007fdd46a249d1 in start_thread () from /lib64/libpthread.so.0
#10 0x00007fdd459a38fd in clone () from /lib64/libc.so.6
登出时相对简单,在close_connection中增加了审计动作。
query审计流程 执行前
#0 audit_null_notify (thd=0x7fdcc7ff6000, event_class=0, event=0x7fdd45440da0) at /mysql56/plugin/audit_null/audit_null.c:99
#1 0x0000000000683f22 in plugins_dispatch (thd=0x7fdcc7ff6000, event_subtype=<value optimized out>, ap=<value optimized out>) at /mysql56/sql/sql_audit.cc:455
#2 event_class_dispatch (thd=0x7fdcc7ff6000, event_subtype=<value optimized out>, ap=<value optimized out>) at /mysql56/sql/sql_audit.cc:491
#3 general_class_handler(THD *, uint, typedef __va_list_tag __va_list_tag *) (thd=0x7fdcc7ff6000, event_subtype=<value optimized out>, ap=<value optimized out>)
at /mysql56/sql/sql_audit.cc:90
#4 0x00000000006841b1 in mysql_audit_notify (thd=0x7fdcc7ff6000, event_class=0, event_subtype=0) at /mysql56/sql/sql_audit.cc:217
#5 0x000000000063e7cb in mysql_audit_general_log (thd=0x7fdcc7ff6000, cmd=0xc76b19 "Query", cmdlen=5, query_str=<value optimized out>, query_len=<value optimized out>)
at /mysql56/sql/sql_audit.h:125
#6 0x000000000063e93e in log_command (thd=0x7fdcc7ff6000, command=<value optimized out>, query=0x7fdcc201c010 "SELECT DATABASE()", query_length=17) at /mysql56/sql/log.cc:2094
#7 general_log_write (thd=0x7fdcc7ff6000, command=<value optimized out>, query=0x7fdcc201c010 "SELECT DATABASE()", query_length=17) at /mysql56/sql/log.cc:2136
#8 0x00000000006e0b37 in mysql_parse (thd=0x7fdcc7ff6000, rawbuf=<value optimized out>, length=<value optimized out>, parser_state=<value optimized out>)
at /mysql56/sql/sql_parse.cc:6578
#9 0x00000000006e1e78 in dispatch_command (command=COM_QUERY, thd=0x7fdcc7ff6000, packet=0x7fdcc7ffa001 "SELECT DATABASE()", packet_length=3254894625)
at /mysql56/sql/sql_parse.cc:1359
#10 0x00000000006a908d in do_handle_one_connection (thd_arg=<value optimized out>) at /mysql56/sql/sql_connect.cc:990
#11 0x00000000006a91c2 in handle_one_connection (arg=<value optimized out>) at /mysql56/sql/sql_connect.cc:906
#12 0x0000000000939337 in pfs_spawn_thread (arg=0x7fdccb3be840) at /mysql56/storage/perfschema/pfs.cc:1860
#13 0x00007fdd46a249d1 in start_thread () from /lib64/libpthread.so.0
#14 0x00007fdd459a38fd in clone () from /lib64/libc.so.6
执行后:
#0 audit_null_notify (thd=0x7fdcc7ff6000, event_class=0, event=0x7fdd45441180) at /mysql56/plugin/audit_null/audit_null.c:99
#1 0x0000000000683f22 in plugins_dispatch (thd=0x7fdcc7ff6000, event_subtype=<value optimized out>, ap=<value optimized out>) at /mysql56/sql/sql_audit.cc:455
#2 event_class_dispatch (thd=0x7fdcc7ff6000, event_subtype=<value optimized out>, ap=<value optimized out>) at /mysql56/sql/sql_audit.cc:491
#3 general_class_handler(THD *, uint, typedef __va_list_tag __va_list_tag *) (thd=0x7fdcc7ff6000, event_subtype=<value optimized out>, ap=<value optimized out>)
at /mysql56/sql/sql_audit.cc:90
#4 0x00000000006841b1 in mysql_audit_notify (thd=0x7fdcc7ff6000, event_class=0, event_subtype=3) at /mysql56/sql/sql_audit.cc:217
#5 0x00000000006e1543 in mysql_audit_general (command=<value optimized out>, thd=0x7fdcc7ff6000, packet=<value optimized out>, packet_length=1) at /mysql56/sql/sql_audit.h:196
#6 dispatch_command (command=<value optimized out>, thd=0x7fdcc7ff6000, packet=<value optimized out>, packet_length=1) at /mysql56/sql/sql_parse.cc:1793
#7 0x00000000006a908d in do_handle_one_connection (thd_arg=<value optimized out>) at /mysql56/sql/sql_connect.cc:990
#8 0x00000000006a91c2 in handle_one_connection (arg=<value optimized out>) at /mysql56/sql/sql_connect.cc:906
#9 0x0000000000939337 in pfs_spawn_thread (arg=0x7fdccb3be840) at /mysql56/storage/perfschema/pfs.cc:1860
#10 0x00007fdd46a249d1 in start_thread () from /lib64/libpthread.so.0
#11 0x00007fdd459a38fd in clone () from /lib64/libc.so.6
Query动作审计工作流程如上图,需要留意的是在语句执行前和执行后都有审计,通过上面的宏MYSQL_AUDIT_GENERAL_LOG和MYSQL_AUDIT_GENERAL_RESULT告诉Audit是执行前后进入的审计模块,基于此可以做一些更细分的功能。当执行结果有错时,通过MYSQL_AUDIT_GENERAL_ERROR来告知审计模块。
实例流程演示: 文字描述抽象,下面以一个select 语句实例演示下审计流程。 执行语句: select from t2; 当语法解析重写完成后,*在execute执行前,会在LOGGER::log_command()中进入审计流程,代码中增加了处理审计逻辑代码:
mysql_audit_general_log(thd, command_name[(uint) command].str,
command_name[(uint) command].length,
query_str, query_length);
不论审计插件有没有安装都会进入这个函数,进入这个函数第一件事就会判断是否有审计插件,没有安装审计会直接跳过所有审计逻辑,这个函数就是上面第3点介绍的获取有用值两个内联函数之一 : mysql_audit_general_log
static inline
void mysql_audit_general_log(THD *thd, const char *cmd, uint cmdlen,
const char *query_str, size_t query_len)
{
if (mysql_global_audit_mask[0] & MYSQL_AUDIT_GENERAL_CLASSMASK) //没装插件则跳过
{
//取得有用的各种值
.....
....
....
mysql_audit_notify(thd, MYSQL_AUDIT_GENERAL_CLASS, MYSQL_AUDIT_GENERAL_LOG,
error_code, time, user, userlen, cmd, cmdlen, query.str,
query.length, clientcs, rows, sql_command, host,
external_user, ip); //把有用的值往下传
}
}
在mysql_audit_notify会判断下是连接语句还是通用语句,然后调用对应的函数,如本例中会调通用语句处理接口general_class_handler
static void general_class_handler(THD *thd, uint event_subtype, void *tb, va_list ap)
{
mysql_event_general event;
....
event.general_query= va_arg(ap, const char *);
event.general_query_length= va_arg(ap, unsigned int);
event.general_charset= va_arg(ap, struct charset_info_st *);
event.general_rows= (unsigned long long) va_arg(ap, ha_rows);
event.general_host= va_arg(ap, MYSQL_LEX_STRING);
event_class_dispatch(thd, MYSQL_AUDIT_GENERAL_CLASS, &event);
}
在general_class_handler函数之后,语句的信息就已经有了,保存在event结构中,上面语句在gdb中 实际打印的结果如下:
(gdb) p (mysql_event_general )event
$2 = {event_subclass = 0, general_error_code = 0, general_thread_id = 1, general_user = 0x7fd27aa51050 “root[root] @ localhost [127.0.0.1]”,
general_user_length = 34, general_command = 0xc76b19 “Query”, general_command_length = 5,
general_query = 0x7fd1f5c1a010 “select from t2”, general_query_length = 15, general_charset = 0x12e27e0, general_time = 1505712354,
general_rows = 0, general_host = {str = 0xbe4d2f “localhost”, length = 9}, general_sql_command = {str = 0xd29b04 “select”, length = 6},
general_external_user = {str = 0xc70579 “”, length = 0}, general_ip = {str = 0x7fd1f5c12040 “127.0.0.1”, length = 9}}
可以非常清楚的看到语句内容,时间,类型等信息。 当 *execute执行后,会在 dispatch_command () 函数尾部会调用审计接口:
if (!thd->is_error() && !thd->killed_errno())
mysql_audit_general(thd, MYSQL_AUDIT_GENERAL_RESULT, 0, 0);
mysql_audit_general(thd, MYSQL_AUDIT_GENERAL_STATUS,
thd->get_stmt_da()->is_error() ?
thd->get_stmt_da()->sql_errno() : 0,
command_name[command].str);
这里调用的是 mysql_audit_general,也是上面第3点介绍的获取有用值另一个内联函数,功能和mysql_audit_general_log相同,带log的为执行前调,不带log的为执行后调,区别在于记录的值略有差异,如执行前的值里没错误状态,执行后的值是有的,成功为0,失败为对应的错误代码。在mysql_audit_general之后流程和上面mysql_audit_general_log一样,不再介绍。 这两个内联函数处理的是Query语句,另外有三个宏MYSQL_AUDIT_NOTIFY_CONNECTION_CONNECT、MYSQL_AUDIT_NOTIFY_CONNECTION_DISCONNECT、MYSQL_AUDIT_NOTIFY_CONNECTION_CHANGE_USER是处理登陆、登出、换用户时用,功能和前两个函数大同小异,不再赘述。
审计是记录历史动作,但有些情况下,光历史记录远远不够。对于应用系统,很多语句都是都是绑定参数运行的,直观审计到的就是一条条带问号的语句,不能看到实际动作,若要还原真实语句,需要联系上下语句分析,非常不便,这就要求审计有一定的语句还原拼接功能,把参数和内容放一起展示。更进一步,假如有人非常了解审计系统,知道审计记录不会永久存放,在某时刻建一个带有破坏性的存储过程,等超期不再保留建立过存储程记录后,执行并删除,真实运行动作将无法通过审计得知,对这类操作,就有了反向追踪运行内容语句的需求,尤其在金融行业中。
另一方面,审计用户也应该是个特权用户,和超级用户互相制约。审计用户应该独立出来,有超级用户不能修改的帐号密码,专门用于审计相关操作,如开关审计,设置审计策略,查阅审计结果,超级用户不能干涉这些动作,保证审计记录的真实性和完整性。审计用户账号应该掌握在客户最核心人手中,以对超级用户活动形成制衡,更加保证数据的安全。
再深入一层,有客户需求可能希望不同用户看到的内容不一样,对敏感数据根据不同用户进行自动过滤,严格来说这已经不再属于审计范围了,属于数据安全问题,需要有更严密的处理逻辑来实现。对于数据库产品来说,基于BLP模型的强制访问控制(MAC)是实现该需求的方法之一,该模型关键在于权限标签,对主客体进行标识,每个主体客体都有自己的标签,权限高的可访问修改低的对象,低权限的不能看到高权限的内容,对每个表、列甚至行都打上标签,对内容过滤可以做到列级直至行级。