写点什么

openGauss 数据库源码解析系列文章—安全管理源码解析(五)

作者:daydayup
  • 2023-08-08
    北京
  • 本文字数:4770 字

    阅读完需:约 16 分钟

openGauss 数据库源码解析系列文章—安全管理源码解析(五)

[openGauss](javascript:void(0);) 2023-08-04 18:01 发表于四川


五、审计与追踪


审计机制和审计追踪机制能够对用户的日常行为进行记录和分析,实现规避风险、提高安全性。


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 个字段。图 23 为审计日志的单条记录示例。



图 23 审计记录示例


对审计日志文件进行读写的函数的代码主要位于“pgaudit.cpp”文件中,其中主要包括两类函数:审计文件的读、写、更新函数;审计记录的增、删、查接口。


首先介绍审计文件的数据结构,如图 24 所示。


openGauss 的审计日志采用文件的方式存储在指定目录中。通过查看目录,可以发现日志主要包括两类文件:形如 0_adt 的审计文件以及名为 index_table 索引文件。



图 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];        /*  审计文件指针   */} Audit
复制代码


索引文件中每一个 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,达到删除审计日志的效果,而不实际删除审计记录的物理数据。带来的效果是执行该函数审计日志文件大小不会减小。


5.2 审计执行


1. 执行原理


审计机制是 openGauss 的内置安全能力之一,openGauss 提供对用户发起的 SQL 行为审计和追踪能力,支持针对 DDL、DML 语句和关键行为(登录、退出、系统启动、恢复)的审计。在每个工作线程初始化阶段把审计模块加载至线程中,其审计的执行原理是把审计函数赋给 SQL 生命周期不同阶段的 Hook(钩子),当线程执行至 SQL 处理流程的特定阶段后会进行审计执行判定逻辑。审计模块加载关键代码如下:


SQL 语句在执行到 ProcessUtility_hook 和 ExecutorEnd_hook 函数指针时,会分别进入到已预置好的审计流程中。这两个函数指针的位置在 SQL 进入执行器执行之前,具体关系如图 25 所示。



图 25 审计执行关系图


如图 25 所示,在线程初始化阶段审计模块已加载完毕。SQL 经过优化器得到计划树,此时审计模块的 pgaudit_ExecutorEnd 函数和 pgaudit_ProcessUtility 函数分别进行 DML 和 DDL 语句的分析,如果和已设置审计策略相匹配,则会调用审计日志接口,生成对应的审计日志。对于系统变更类的审计直接内置于相应行为的内核代码中。


2. 关键执行流程


  1. 系统变更类审计执行


以上为 openGauss 支持系统变更类的审计执行函数,对于此类审计函数均嵌入内核相应调用流程中,下面以审计用户登入退出 pgaudit_user_login 函数为例说明其主体流程。



图 26 登入审计执行流程


图 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 逻辑进行审计日志执行并最终记录审计日志。



图 27 DDL 审计执行流程


如图 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 函数,整体调用流程如图 28 所示。



图 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 语句的审计日志执行完成。

用户头像

daydayup

关注

还未添加个人签名 2023-07-18 加入

还未添加个人简介

评论

发布
暂无评论
openGauss数据库源码解析系列文章—安全管理源码解析(五)_daydayup_InfoQ写作社区