在前面文章中介绍过“9.4 对象权限管理”,本篇我们介绍第9章 安全管理源码解析中“9.5 审计与追踪”的相关精彩内容介绍。
审计机制和审计追踪机制能够对用户的日常行为进行记录和分析,实现规避风险、提高安全性。
9.5.1 审计日志设计
审计内容的记录方式通常有两种:记录到数据库的表中、记录到OS文件中。openGauss采用记录到OS文件中(即审计日志)的方式来保存审计结果,审计日志文件夹受操作系统权限保护,默认只有初始化用户可以读写,从数据库安全角度出发,保证了审计结果的可靠性。日志文件的存储目录由audit_directory参数指定。
openGauss审计日志每条记录包括time、type、result、userid、username、database、client_conninfo、object_name、detail_info、node_name、thread_id、local_port、remote_port共13个字段。图9-23为审计日志的单条记录示例。
图9-23 审计记录示例
对审计日志文件进行读写的函数的代码主要位于“pgaudit.cpp”文件中,其中主要包括两类函数:审计文件的读、写、更新函数;审计记录的增、删、查接口。
首先介绍审计文件的数据结构,如图9-24所示。
openGauss的审计日志采用文件的方式存储在指定目录中。通过查看目录,可以发现日志主要包括两类文件:形如0_adt的审计文件以及名为index_table索引文件。
图9-24 审计文件结构
以adt结尾的审计文件中,每一条审计记录对应一个AuditData结构体。数据结构AuditData代码如下:
typedef struct AuditData {
AuditMsgHdr header; /* 记录文件头,存储记录的标识、大小等信息 */
AuditType type; /* 审计类型 */
AuditResult result; /* 执行结果 */
char varstr[1]; /* 二进制格式存储的具体审计信息 */
} AuditData;
其中AuditMsgHdr记录着审计记录的标识信息,数据结构AuditMsgHdr的代码如下:
typedef struct AuditMsgHdr {
char signature[2]; /* 审计记录标识,目前固定为AUDIT前两个字符’A’和’U’ */
uint16 version; /* 版本信息,目前固定为0 */
uint16 fields; /* 审计记录字段数,目前为13 */
uint16 flags; /* 记录有效性标识,如果被删除则标记为DEAD */
pg_time_t time; /* 审计记录创建时间 */
uint32 size; /* 审计信息占字节长度 */
} AuditMsgHdr;
AuditData的其他结构存储着审计记录的审计信息,AuditType为审计类型,目前有38种类型。AuditResult为执行的结果,有AUDIT_UNKNOWN、AUDIT_OK、AUDIT_FAILED三种结果。其余的各项信息,均通过二进制的方式写入到varstr中。
审计日志有关的另一个文件为索引文件index_table,其中记录着审计文件的数量、审计日志文件编号、审计文件修改日期等信息。其数据结构AuditIndexTable代码如下:
typedef struct AuditIndexTable {
uint32 maxnum; /* 审计目录下审计文件个数的最大值 */
uint32 begidx; /* 审计文件开始编号 */
uint32 curidx; /* 当前使用的审计文件编号 */
uint32 count; /* 当前审计文件的总数 */
pg_time_t last_audit_time; /* 最后一次写入审计记录的时间 */
AuditIndexItem data[1]; /* 审计文件指针 */
} AuditIndexTable;
索引文件中每一个AuditIndexItem对应一个审计文件,其数据结构AuditIndexItem的代码如下:
typedef struct AuditIndexItem {
pg_time_t ctime; /* 审计文件创建时间 */
uint32 filenum; /* 审计文件编号 */
uint32 filesize; /* 审计文件占空间大小 */
} AuditIndexItem;
审计文件的读、写类函数如auditfile_open、auditfile_rotate等函数实现较简单,读者可以直接阅读源码。
下面主要介绍日志文件的结构和日志记录的增、删、查接口。
审计记录的写入接口为audit_report函数。该函数的原型为:
void audit_report(AuditType type, AuditResult result, const char* object_name, const char* detail_info);
其中入参type、result、object_name、detail_info分别对应审计日志记录中的相应字段,审计日志中的其余9个字段均为函数在执行时从全局变量中获取。
audit_report函数的执行主要分为3个部分,首先会检查审计的各项开关,判断是否需要审计该操作;然后根据传入的参数、全局变量中的参数以及当前时间,生成审计日志所需的信息并拼接成字符串;最后调用审计日志文件读写接口,将审计日志写入文件中。
审计记录查询接口为pg_query_audit函数,该函数为数据库内置函数,可供用户直接调用,调用形式为:
SELECT * FROM pg_query_audit (timestamptz startime,timestamptz endtime, audit_log);
入参为需要查询审计记录的起始时间和终止时间以及审计日志文件所在的物理路径。当不指定audit_log时,默认查看连接当前实例的审计日志信息。
审计记录的删除接口为pg_delete_audit函数,该函数为数据库内置函数,可供用户直接调用,调用形式为:
SELECT * FROM pg_delete_audit (timestamptz startime,timestamptz endtime);
入参为需要被删除审计记录的起始时间和终止时间。该函数通过调用pgaudit_delete_file函数来将审计日志文件中,startime与endtime之间的审计记录标记为AUDIT_TUPLE_DEAD,达到删除审计日志的效果,而不实际删除审计记录的物理数据。带来的效果是执行该函数审计日志文件大小不会减小。
9.5.2 审计执行
1. 执行原理
审计机制是openGauss的内置安全能力之一,openGauss提供对用户发起的SQL行为审计和追踪能力,支持针对DDL、DML语句和关键行为(登录、退出、系统启动、恢复)的审计。在每个工作线程初始化阶段把审计模块加载至线程中,其审计的执行原理是把审计函数赋给SQL生命周期不同阶段的Hook(钩子),当线程执行至SQL处理流程的特定阶段后会进行审计执行判定逻辑。审计模块加载关键代码如下:
void pgaudit_agent_init(void) {
…
/* DDL、DML语句审计Hook赋值, 赋值结束后标识审计模块已在此线程加载 */
prev_ExecutorEnd = ExecutorEnd_hook;
ExecutorEnd_hook = pgaudit_ExecutorEnd;
prev_ProcessUtility = ProcessUtility_hook;
ProcessUtility_hook = (ProcessUtility_hook_type)pgaudit_ProcessUtility;
u_sess->exec_cxt.g_pgaudit_agent_attached = true;
}
SQL语句在执行到ProcessUtility_hook和ExecutorEnd_hook函数指针时,会分别进入到已预置好的审计流程中。这两个函数指针的位置在SQL进入执行器执行之前,具体关系如图9-25所示。
图9-25 审计执行关系图
如图9-25所示,在线程初始化阶段审计模块已加载完毕。SQL经过优化器得到计划树,此时审计模块的pgaudit_ExecutorEnd函数和pgaudit_ProcessUtility函数分别进行DML和DDL语句的分析,如果和已设置审计策略相匹配,则会调用审计日志接口,生成对应的审计日志。对于系统变更类的审计直接内置于相应行为的内核代码中。
2. 关键执行流程
- 系统变更类审计执行
pgaudit_system_recovery_ok
pgaudit_system_start_ok
pgaudit_system_stop_ok
pgaudit_user_login
pgaudit_user_logout
pgaudit_system_switchover_ok
pgaudit_user_no_privileges
pgaudit_lock_or_unlock_user
以上为openGauss支持系统变更类的审计执行函数,对于此类审计函数均嵌入内核相应调用流程中,下面以审计用户登入退出pgaudit_user_login函数为例说明其主体流程。
图9-26 登入审计执行流程
图9-26为服务端校验客户端登入时的主要流程。以登录失败场景为例,首先根据配置文件和客户端IP和用户信息确认采用的认证方式(包括sha256和SSL认证等);然后根据不同的认证方式采用不同的认证流程和客户端进行交互完成认证身份流程;如果认证失败,则线程进入退出流程上报客户端,此时调用pgaudit_user_login获取当前访问数据库名称和详细信息,并记录登录失败相关的审计日志。关键代码如下:
/* 拼接登录失败时候的详细信息,包括数据库名称和用户名 */
rc = snprintf_s(details,
PGAUDIT_MAXLENGTH,
PGAUDIT_MAXLENGTH - 1,
"login db(%s)failed,authentication for user(%s)failed",
port->database_name,
port->user_name);
securec_check_ss(rc, "\0", "\0");
/* 调用登入审计函数,记录审计日志 */
pgaudit_user_login(FALSE, port->database_name, details);
/* 退出当前线程 */
ereport(FATAL, (errcode(errcode_return), errmsg(errstr, port->user_name)))
登入审计日志接口pgaudit_user_login则主要完成审计日志记录接口需要参数的拼接,相关代码如下:
void pgaudit_user_login(bool login_ok, const char* object_name, const char* detaisinfo)
{
AuditType audit_type;
AuditResult audit_result;
Assert(detaisinfo);
/* 审计类型和审计结果拼装 */
if (login_ok) {
audit_type = AUDIT_LOGIN_SUCCESS;
audit_result = AUDIT_OK;
} else {
audit_type = AUDIT_LOGIN_FAILED;
audit_result = AUDIT_FAILED;
}
/* 直接调用审计日志记录接口 */
audit_report(audit_type, audit_result, object_name, detaisinfo);
}
2) DDL、DML语句审计执行
依据“1. 执行原理”节的描述,DDL、DML语句的执行分别由于pgaudit_ProcessUtility函数、pgaudit_ExecutorEnd函数来承载。此处首先介绍函数pgaudit_ProcessUtility函数,其主体结构代码如下:
static void pgaudit_ProcessUtility(Node* parsetree, const char* queryString, ...)
{
/* 适配不同编译选项 */
...
/* 开始匹配不同的DDL语句 */
switch (nodeTag(parsetree)) {
case T_CreateStmt: {
/* CREATE table语句审计执行 */
CreateStmt* createtablestmt = (CreateStmt*)(parsetree);
pgaudit_ddl_table(createtablestmt->relation->relname, queryString);
} break;
case T_AlterTableStmt: {
AlterTableStmt* altertablestmt = (AlterTableStmt*)(parsetree); /* Audit alter table */
if (altertablestmt->relkind == OBJECT_SEQUENCE) {
pgaudit_ddl_sequence(altertablestmt->relation->relname, queryString);
} else {
pgaudit_ddl_table(altertablestmt->relation->relname, queryString);
}
} break;
/* 匹配其他DDL类型语句逻辑 */
...
}}
DDL审计执行函数关键入参parsetree用于识别审计日志类型(create/drop/alter等操作)。入参queryString保存原始执行SQL语句,用于记录审计日志,略去非关键流程。此函数主要根据判断nodeTag所归属的DDL操作类型,进入不同的审计执行逻辑。以T_CreateStmt为例,识别当前语句CREATE table则进入pgaudit_ddl_table逻辑进行审计日志执行并最终记录审计日志。
图9-27 DDL审计执行流程
如图9-27所示,首先从当前SQL语句中获取执行对象类别校验其相应的审计开关是否开启(可以通过GUC参数audit_system_object控制)。当前支持开启的全量对象代码如下:
typedef enum {
DDL_DATABASE = 0,
DDL_SCHEMA,
DDL_USER,
DDL_TABLE,
DDL_INDEX,
DDL_VIEW,
DDL_TRIGGER,
DDL_FUNCTION,
DDL_TABLESPACE,
DDL_RESOURCEPOOL,
DDL_WORKLOAD,
DDL_SERVERFORHADOOP,
DDL_DATASOURCE,
DDL_NODEGROUP,
DDL_ROWLEVELSECURITY,
DDL_TYPE,
DDL_TEXTSEARCH,
DDL_DIRECTORY,
DDL_SYNONYM
} DDLType;
如果DDL操作的对象审计已开启则进行审计日志记录流程,在调用审计日志记录函数audit_report之前需要对包含密码的SQL语句进行脱敏处理。将包含密码的语句中(CREATE role/user)密码替换成‘********’用于隐藏敏感信息,至此针对CREATE DDL语句的审计执行完成。其他类型DDL语句主体流程一致,不做赘述。
下面介绍针对DML语句审计执行逻辑pgaudit_ExecutorEnd函数,整体调用流程如图9-28所示。
图9-28 DML审计执行流程
首先判断SQL查询语句所归属的查询类型。以CMD_SELECT类型为例,先获取查询对象的object_name用于审计日志记录中访问对象的记录,然后调用pgaudit_dml_table函数。相关代码如下:
case CMD_SELECT:
object_name = pgaudit_get_relation_name(queryDesc->estate->es_range_table);
pgaudit_dml_table_select(object_name, queryDesc->sourceText);
和DDL的记录一样,同样会对敏感信息进行脱敏后调用审计日志记录接口audit_report,至此对DML语句的审计日志执行完成。
感谢大家学习第9章 安全管理源码解析中“9.5 审计与追踪”的精彩内容,下一篇我们开启“9.6 数据安全技术”的相关内容的介绍。
敬请期待。