书接上文数据库高安全—审计追踪:传统审计&统一审计,从传统审计和统一审计两方面对高斯数据库的审计追踪技术进行解读,本篇将从数据动态脱敏方面对高斯数据库的数据保护技术进行解读。
5.1 数据动态脱敏
数据脱敏,顾名思义就是将敏感数据通过变形、屏蔽等方式处理,其目的是保护隐私数据信息,防止数据泄露和恶意窥探。当企业或者机构收集用户个人身份数据、手机、银行卡号等敏感信息,然后将数据通过导出(非生产环境)或直接查询(结合生产环境)的方式投入使用时,按照隐私保护相关法律法规需将数据进行“脱敏”处理。
openGauss实现了数据动态脱敏机制,它根据一系列用户配置的“脱敏策略”来对查询命令进行分析匹配,最终将敏感数据屏蔽并返回。使用数据动态脱敏特性总的来说分为两个步骤:配置脱敏策略、触发脱敏策略。本小节将对这两个步骤进行具体分析。
显然,只有在配置脱敏策略后系统才能有根据地进行敏感数据脱敏。openGauss提供了脱敏策略配置(创建、修改、删除)语法,这些语法所涉及的语法解析结点内容大致相同,因此这里仅对创建策略相关数据结构进行分析,其余不再赘述。下面将结合一个具体示例对数据动态脱敏特性进行详细介绍。
表1给出了一张包含敏感信息(薪资、银行卡号)的个人信息表,策略管理员要对该表中的敏感信息创建脱敏策略:当用户user1或user2在IP地址10.123.123.123上使用jdbc或gsql连接数据库并查询个人信息表时,系统将自动屏蔽敏感信息。
表1 个人信息表person
id | name | gender | salary | creditcards |
1 | 张三 | 男 | 10000 | 6210630600006321083 |
2 | 李四 | 男 | 15000 | 6015431250003215514 |
3 | 王五 | 女 | 20000 | 5021134522201529881 |
首先策略管理员需要对敏感列打标签,随后使用标签创建脱敏策略,策略配置DDL语句如下:
例1脱敏策略配置示例。
配置资源标签:
CREATE RESOURCE LABEL salary_label ADD COLUMN(person.salary);
CREATE RESOURCE LABEL creditcard_label ADD COLUMN(person.creditcards);
配置脱敏策略:
CREATE MASKING POLICY mask_person_policy MASKALL ON LABEL(salary_label), CREDITCARDMASKING ON label(creditcard_label) FILTER ON ROLES(user1,user2), IP(‘10.123.123.123’), APP(jdbc, gsql);
user1在10.123.123.123地址使用gsql查询敏感数据:
SELECT id, salary, creditcards from public.person;
下面将对CREATE MASKING POLICY语句所涉及的语法结构定义进行逐一介绍。
数据结构CreateMaskingPolicyStmt:
typedef struct CreateMaskingPolicyStmt
{
NodeTag type;
char *policy_name; // 脱敏策略名称
List *policy_data; // 脱敏策略行为
List *policy_filters; // 用户过滤条件
bool policy_enabled; // 策略开关
} CreateMaskingPolicyStmt;
脱敏策略创建语法是对CreateMaskingPolicyStmt进行填充,其中policy_data是由若干DefElem节点组成的List,每个DefElem指出了以何种方式脱敏数据库资源,DefElem->name标识脱敏方法,DefElem->arg代表脱敏对象。
数据动态脱敏中例1脱敏策略配置示例的步骤(3) 对应的policy_data组织结构如图1所示。
图1 脱敏策略配置示例对应的policy_data组织结构
policy_filters描述了脱敏策略生效的用户场景(用户名、客户端、登录IP),该List只有一个节点,是Policy Filter前缀逻辑树的根节点,只有当用户信息满足逻辑树所描述的场景时,其相应的脱敏策略才会生效。Policy Filter逻辑树节点如下所示:
typedef struct PolicyFilterNode
{
NodeTag type;
char *node_type; // 逻辑操作类型,取值为“op”或“filter”
char *op_value; // 逻辑操作符,仅当node_type为op时取值为“and”或“or”,否则为NULL
char *filter_type;// 过滤数据类型,仅当node_type为filter时取值为“APP”、“ROLES”、“IP”
List *values; // 过滤数据值List,指出具体的过滤条件值,若node_type为op时置NULL
Node *left; // 左子树
Node *right; // 右子树
} PolicyFilterNode;
逻辑树节点分为操作符(op)节点和过滤数据(filter)节点,当op节点分为“与”或“或”关系,其op_value将置为“and”或“or”,其左右子树代表操作符左右子表达式。filter节点一般作为op的叶子节点出现,它标识具体的过滤信息并将其值存放在values链表中。需要注意的是,一个节点不可能既是op节点又是filter节点。数据动态脱敏中例1脱敏策略配置示例的步骤(3) 对应的policy_filters组织结构如图2所示。
图2 配置脱敏策略对应的policy_filter组织结构
脱敏策略配置的总体流程如图3所示。
图3 脱敏策略配置流程图
在查询编译脱敏策略配置SQL之后将进入策略增删改主函数中,首先会根据语法解析节点校验相关参数的合法性,做如下检查:
-
检查脱敏策略指定的数据库资源是否存在。
-
检查脱敏函数是否存在。
-
检查脱敏策略是否已存在。
-
检查脱敏相关约束:脱敏对象必须为基本表的数据列、脱敏列类型必须满足规格限制、脱敏列只允许加载一个脱敏函数。
-
检查Masking Filter是否冲突,不允许同一数据库资源在相同用户场景下触发多个策略。
其中Masking Filter冲突校验的目的是为了防止用户场景同时满足多个脱敏策略限制,导致策略匹配时系统无法判断应该触发哪种脱敏策略。因此在创建策略时要保证其Filter与现存的策略互斥,主要是判断是否存在一种用户场景能够同时满足多个MASKING FILTER。在数据动态脱敏所示的表2数据基础上,如下表中策略A和策略B是相互冲突的,而策略A和策略C是互斥的。
脱敏策略冲突或互斥场景:
策略A:CREATE MASKING POLICY mask_A MASKALL ON LABEL(creditcard_label) FILTER ON IP(’10.123.123.123’), APP(jdbc), ROLES(user1);
策略B:CREATE MASKING POLICY mask_B CREDITCARDMASKING ON LABEL(creditcard_label) FILTER ON IP(’10.123.123.123’,’10.90.132.132’), APP(jdbc, gsql), ROLES(user1);
策略C:CREATE MASKING POLICY mask_C CREDITCARDMASKING ON LABEL(creditcard_label) FILTER ON IP(’10.123.123.123’ ,’10.90.132.132’), APP(jdbc), ROLES(user2);
随后将依据策略配置信息更新系统表:
-
更新gs_masking_policy系统表,存储policy基本信息。
-
更新gs_masking_policy_actions系统表,存储策略对应的脱敏方式及脱敏对象。
-
更新gs_masking_policy_filter系统表,存储脱敏用户场景过滤信息,此时会将逻辑树转换为逻辑表达式字符串进行存储,在之后的敏感数据访问时该字符串将会重新转换为逻辑树进行场景校验。
为了降低策略读取IO损耗,openGauss维护了一组线程级别的策略缓存,用于保存已配置的脱敏策略,并在策略配置后进行实时刷新。
在用户进行数据查询时,数据动态脱敏特性使用openGauss的HOOK机制,将查询编译生成的查询树(Query)钩取出来与脱敏策略进行匹配,最后将查询树按照脱敏策略内容改写成不包含敏感数据的“脱敏”查询树返还给解析层继续执行,最终实现屏蔽敏感数据的能力。其执行流程图如4所示。
图4 脱敏策略执行流程图
在对一个访问数据库资源的查询树进行脱敏之前,需要准备一份待匹配的脱敏策略集合,其依据就是用户登录信息,check_masking_policy_filter函数的任务就是将用户信息与所有的脱敏策略进行匹配,筛选出可能被查询触发的脱敏策略。最终筛选如下脱敏策略:
-
若脱敏策略没有配置FILTER信息,说明对所有用户生效。
-
若当前用户信息与脱敏策略的FILTER匹配,则说明对当前用户生效。
在每个脱敏策略从系统表读入缓存时,需要将对应的FILTER逻辑表达式转换为逻辑树并将逻辑树根节点存入缓存中,将其作为脱敏策略筛选条件,逻辑树结构如下。
class PolicyLogicalTree {
public:
…
bool parse_logical_expression(const gs_stl::gs_string logical_expr_str); // 逻辑表达式构造逻辑树入口函数
bool match(const FilterData *filter_item);
bool has_intersect(PolicyLogicalTree *arg);
private:
gs_stl::gs_vector<PolicyLogicalNode> m_nodes; // 逻辑节点集合,包含了逻辑树中所有的节点
gs_stl::gs_vector<int> m_flat_tree; // 利用数组将逻辑节点索引构造逻辑二叉树
// 逻辑表达式转换为逻辑树的递归函数。
bool parse_logical_expression_impl(const gs_stl::gs_string logical_expr_str, int *offset, int *idx, Edirection direction);
inline void create_node(int *idx, EnodeType type, bool has_operator_not); // 创建单个逻辑树节点
void flatten_tree(); // 将逻辑树刷新到m_nodes集合与m_flat_tree索引中
bool check_apps_intersect(string_sort_vector*, string_sort_vector*);
bool check_roles_intersect(oid_sort_vector*, oid_sort_vector*);
bool m_has_ip; // 标识整个逻辑树是否涉及ip校验
bool m_has_role; // 标识整个逻辑树是否涉及用户名校验
bool m_has_app; // 标识整个逻辑树是否涉及客户端校验
};
逻辑树节点的结构与语法解析中的FILTER节点类似,具体可以参照PolicyFilterNode结构。
struct PolicyLogicalNode {
...
EnodeType m_type;
int m_left; // 左子节点索引
int m_right; // 右子节点索引
void make_eval(const FilterData *filter_item); //判断用户信息是否满足本节点子树表示的逻辑。
bool m_eval_res;
oid_sort_vector m_roles; // 本节点包含的用户名集合
string_sort_vector m_apps; // 本节点包含的客户端名称集合
IPRange m_ip_range; //本节点包含的IP
};
当需要将逻辑表达式转变为逻辑树时,parse_logical_expression_impl函数将对逻辑表达式字符串进行递归解析,识别出表达式包含的操作符(and或or)以及Filter信息(ip、roles、app),构造出PolicyLogicalNode并使用左右子节点索引(m_left、m_right)链接起来形成逻辑树并将每个节点存入m_nodes中,最终利用m_nodes构造m_flat_tree数组来模拟二叉树。
m_flat_tree数组的作用是标记逻辑树节点间关系以及标识哪些节点是逻辑树的叶子节点,当用户信息与逻辑树某节点进行匹配时,首先需要与其左右子树进行匹配,然后根据该节点的逻辑运算符来判断是否满足Filter要求,而左右子树的判断结果又依赖于它们的子树的结果,因此这种递归判断方法首先将会是取叶子节点进行用户信息匹配。
openGauss使用“自底向上”的方式来进行用于信息与逻辑树的匹配,从m_flat_tree末尾即叶子节点进行匹配,将匹配结果记录下来,当匹配到非叶子节点时(op节点)只需使用其左右子节点结果进行判断即可,最终实现整个逻辑树的匹配。在例1脱敏策略配置示例中创建脱敏策略后,当用户使用非受限的客户端访问敏感数据时,逻辑树匹配结果如图5所示。
图5 逻辑树匹配示例
在筛选出脱敏策略后,就需要对查询树所有TargetEntry进行识别和策略匹配,从openGauss源码可以看到,支持对SubLink、Aggref、OpExpr、RelabelType、FuncExpr、CoerceViaIO、Var类型的节点进行解析识别。数据脱敏的核心思路是,Var类型节点代表了访问的数据库资源,而非Var类型节点可能包含Var节点,因此需要根据其参数递归的寻找Var节点,最后将识别到的所有Var节点进行策略匹配并根据策略内容进行节点替换。
识别脱敏结点源码:
static bool mask_expr_node(ParseState *pstate, Expr*& expr,
const policy_set *policy_ids, masking_result *result, List* rtable, bool can_mask)
{
if (expr == NULL) {
return false;
}
switch (nodeTag(expr)) {
case T_SubLink:
... // 解析SubLink结点
case T_FuncExpr:
... // 解析FuncExpr结点
case T_Var:
return handle_masking_node(pstate, expr, policy_ids, result, rtable, can_mask); // 进入最后脱敏处理过程
break;
case T_RelabelType:
... // 解析RelabelType结点
case T_CoerceViaIO:
... // 解析CoerceViaIO结点
case T_Aggref:
... // 解析Aggref结点
case T_OpExpr:
... // 解析OpExpr结点
default:
break;
}
return false;
}
在匹配脱敏策略时,首先需要将识别出的Var节点进行解析,将其转为PolicyLabelItem,该数据结构存储了数据列的全部路径信息,然后将其与已过滤出的脱敏策略集合进行匹配,若某个脱敏策略对应的数据库资源对象与PolicyLabelItem一致,将已匹配到的脱敏策略指定的方式替换该Var节点,相关数据结构如下:数据结构PolicyLabelItem。
struct PolicyLabelItem {
...
void get_fqdn_value(gs_stl::gs_string *value) const;
bool operator < (const PolicyLabelItem& arg) const;
bool operator == (const PolicyLabelItem& arg) const;
bool empty() const {return strlen(m_column) == 0;}
void set_object(const char *obj, int obj_type = 0);
void set_object(Oid objid, int obj_type = 0);
Oid m_schema; // 数据库资源所属的namespace Oid
Oid m_object; // 数据库资源所属的table Oid
char m_column[256];// 列名
int m_obj_type; // 资源类型,数据动态脱敏仅支持对COLUMN生效
};
脱敏策略匹配成功后,将会根据策略内容替换包含敏感信息的Var节点,使之外嵌脱敏函数。最后将修改后的查询树返还给解析器继续执行,最终敏感数据将会在脱敏函数的作用下返回,以脱敏的形式返回给客户端。数据动态脱敏中例1脱敏策略配置示例里使用gsql查询敏感数据的步骤(4) 中SELECT语句对应的查询树脱敏前的数据组织结构如图6所示。
图6 脱敏结点替换示例
至此,整个查询树已经完成了脱敏策略的匹配与重写,随后将重新回归查询解析模块并继续执行后续处理,最终系统将返回脱敏后的数据结果。
以上内容从数据动态脱敏方面对高斯数据库的数据保护技术进行了解读,下篇讲从数据透明加密方面继续介绍高斯数据库的数据保护。