PostgreSQL 安全管理 数据脱敏 Anonymizer 脱敏函数
1 背景知识
本章将介绍 Anonymizer 提供的提供了8种脱敏函数。
- 数据破坏(Destruction)。
- 添加噪音(Adding Noise)。
- 随机化(Randomization)。
- 伪造(Faking)。
- 高级伪造(Advanced Faking)。
- 假名化(Pseudonymization)。
- 通用哈希(Generic Hashing)。
- 部分脱敏(Partial scrambling)。
- 条件脱敏(Conditional masking)。
- 泛化(Generalization)。
- 使用 pg_catlog 函数。
- 专门写一个的脱敏函数。
根据数据类型的不同,对于不同的列使用不同的策略。
- 对于
姓名
或者直接标识符
,高级伪造(Advanced Faking) 通常非常有用。 - 对于外键适合于 随机排列(Shuffing)。
- 对于
数值
和日期
数据类型, 非常适合添加噪音(Adding Noise)`。 - 对于
email
和电话号码
数据类型,非常适合部分脱敏(Partial Scrambling)。 - 等等。
2 数据销毁函数(Destruction)
首先,对数据进行最快,最安全的方法就是销毁数据。
隐藏列的内容的最佳方法就是使用一个静态值替换所有的值。
例如:你可以使用 'CONFIDENTIAL'
替换字段内容。
SECURITY LABEL FOR anon
ON COLUMN people.phone
IS 'MASKED WITH VALUE ''CONFIDENTIAL'' ';
3 噪音干扰函数(Adding Noise)
噪音脱敏也称之为方差。通过偏移日期和数值到达数据脱敏的效果。
例如,通过对工资应用 +/- 10%
的方差,确保数据集保持有意义。
anon.noise(original_value,ratio)
:函数 original_value
可以是 int
、bigint
、double precision
。如果噪音比例为 0.33
,返回值将会按照 +/-33%
偏移。
SECURITY LABEL FOR anon ON COLUMN people.id IS 'MASKED WITH FUNCTION anon.noise(id, 0.33)';
anon.dnoise(original_value, interval)
:函数 original_value
可以是 date
,timestamp
,或者 time
。如果 interval='2 days'
,返回值将会按照 +/- 2 days
偏移。
SECURITY LABEL FOR anon ON COLUMN people.date IS 'MASKED WITH FUNCTION anon.dnoise(date,$2 days$)';
类似的函数有 add_noise_on_numeric_column 和 add_noise_on_datetime_column。
4 随机化函数(Randomization)
此扩展提供了大量的函数用于生成随机数据。
4.1 基本随机值函数
anon.random_date()
返回一个随机日期anon.random_string(n)
返回包含n
个字母的TEXT
值。anon.random_zip()
随机返回5
位数代码。anon.random_phone(p)
返回以"p"
为前缀的8位电话号码。anon.random_hash(seed)
返回随机字符串的哈希值。
4.2 数区间中随机值函数
在数据区间中随机取值
anon.random_date_between(d1,d2)
返回d1
andd2
之间的日期。anon.random_int_between(i1,i2)
返回i1
andi2
之间的整数。anon.random_bigint_between(b1,b2)
返回b1
andb2
之间的大整数。
这些函数都包含下限和上限。例如,anon.random_int_between(1,3)
返回值是包含1、2、3.
4.3 随机数组函数
ramdom_in
函数将会随机返回数组中的一个元素。
anon.random_in(ARRAY[1,2,3])
返回中其中的一个数字。anon.random_in(ARRAY['red','green','blue'])
返回其中一个文本。
4.4 随机枚举函数
对于数据使用枚举类型特别有用。
anon.random_in_enum(variable_of_an_enum_type)
返回枚举中的任何值。
CREATE TYPE card AS ENUM ('visa', 'mastercard', ‘amex’);
SELECT anon.random_in_enumCARD;
//屏幕输出:
random_in_enum
----------------
mastercard
CREATE TABLE customer (
id INT,
...
credit_card CARD
);
SECURITY LABEL FOR anon ON COLUMN customer.creditcard
IS 'MASKED WITH FUNCTION anon.random_in_enum(creditcard)'
4.5 随机区间函数
对于 RANGE
数据类型可以描述区间值的强大方式,其中定义包含或排除边界。
PostgreSQL: Documentation: 16: 8.17. Range Types
对于每个 RANGE
数据类型都有一个函数。
anon.random_in_int4range('[5,6)')
返回值为5
。anon.random_in_int8range('(6,7]')
返回值为7
。anon.random_in_numrange('[0.1,0.9]') 返回在
0.1和
0.9之间的
NUMERIC `。anon.random_in_daterange('[2001-01-01, 2001-12-31)')
返回2001
年的日期。anon.random_in_tsrange('[2022-10-01,2022-10-31]')
返回10月的某一天,格式为TIMESTAMP
。anon.random_in_tstzrange('[2022-10-01,2022-10-31]')
返回10月的某一天,格式为带时区的TIMESTAMP
。
如果 RANGE
无边界,则无法获取随机值。例如,anon.random_in_int4range('[2022,)')
返回值为NULL。
5 伪造(Faking)
伪造脱敏的原理是使用随机但可信的数值替换敏感数据。这样做的目的避免正式数据泄露。但是仍然适合测试、数据分析和数据处理。
要使用伪造函数,必须现在数据中启动扩展引擎。
SELECT anon.init();
init()
函数导入一个包含随机数据(银行账户、姓名、城市等)默认数据集。
此数据集为英文版,数据量较小(每个类别为1000个值)。如果你想使用本地化数据或者加载特定数据集,请阅读 自定义伪造数据。
anon.fake_address()
返回完整的地址。anon.fake_city()
返回城市名称。anon.fake_country()
返回国家名称。anon.fake_company()
返回公司名称。anon.fake_email()
返回email
地址。anon.fake_first_name()
返回first_name
名字。anon.fake_iban()
返回国际银行的 IBAN 号码。anon.fake_last_name()
返回last_name
名字。anon.fake_postcode()
返回邮政编码。anon.fake_siret()
返回企业经营活动的 Siret 号码。
对于 TEXT
和 VARCHAR
列,可以使用经典的 Lorem Ipsum 生成器。
anon.lorem_ipsum()
返回 5 个段落文字。anon.lorem_ipsum(2)
返回 2 个段落文字。anon.lorem_ipsum( paragraphs := 4 )
返回 4 个段落文字。anon.lorem_ipsum( words := 20 )
返回 20 个单词。anon.lorem_ipsum( characters := 7 )
返回 7 个字符。anon.lorem_ipsum( characters := anon.length(table.column) )
返回与某个列中相同的数量的字符。
6 高级伪造
伪造数据是一个复杂的主题。对于高级的伪造数据,请使用使用扩展程序 PostgreSQL Faker,此扩展是基于 Faker python library 基础做的二次开发。
CREATE SCHEMA faker;
CREATE EXTENSION faker SCHEMA faker;
SELECT faker.faker('de_DE');
SELECT faker.first_name_female();
//屏幕输出:
first_name_female
-------------------
Mirja
7 假名化(Pseudonymization)
假名化和伪造类似,都是生成类似的真实值。区别在于假名化返回值是确定的:函数会根据 SEED
和 SLAT
参数返回相同的假名化。
要使用伪造函数,必须现在数据中启动扩展引擎。
SELECT anon.init();
anon.pseudo_first_name(seed,salt)
返回一个first_name
值。anon.pseudo_last_name(seed,salt)
返回一个last_name
值。anon.pseudo_email(seed,salt)
返回一个email
值。anon.pseudo_city(seed,salt)
返回一个城市名称。anon.pseudo_country(seed,salt)
返回一个国家名称。anon.pseudo_company(seed,salt)
返回一个公司名称。anon.pseudo_iban(seed,salt)
返回国际银行的 IBAN 号码。anon.pseudo_siret(seed,salt)
返回企业经营活动的 Siret 号码。
第二个参数 SALT
是可选的,设置这个值是为了增加脱敏的复杂性,避免字典攻击和暴力攻击。如果没有给定 SALT
值,则会使用 anon.salt GUC
参数替换。请见 通用哈希 章节。
第一个参数 SEED
是与生成数据的主题相关的信息。例如,我们可以使用某人的登录名作为 SEED
,为其生成伪造的电子邮件地址。
SECURITY LABEL FOR anon
ON COLUMN users.emailaddress
IS 'MASKED WITH FUNCTION anon.pseudo_email(users.login) ';
如果想要使用假名化函数生成唯一值,你需要自定义一个比现有真实数据大得多的自定义伪造数据集。否则很有可能会出现重复,即会出现相同的伪列值。
假名化与匿名化经常被混淆,但实际上他们有两个不同的目的:假名化任与真实数据相关联。GDPR
8 通用哈希(Generic hashing)
从理论上将,哈希不是一种有效的脱敏技术,然而在实际生产中有必要生成原始数据的哈希值。
例如,对于一对主外键,可以保持参照完整性不变进行脱敏。
anon.digest(value,salt,algorithm)
自定义SLAT
和哈希算法,并返回一个哈希值。anon.hash(value)
将会使用默认的SALT
和哈希算法,并返回一个哈希值。anon.algorithm
的默认值为sha256
。可能的值: md5, sha1, sha224, sha256, sha384 or sha512。anon.salt
默认值为空. 你可以使用以下语句进行自定义。
ALTER DATABASE foo SET anon.salt TO 'xsfnjefnjsnfjsnf';
ALTER DATABASE foo SET anon.algorithm TO 'sha384';
哈希化不是真正的脱敏方式。因为可以使用 SALT
和 algorithm
这两个元素,进行暴力反向破解。所以 SALT
和 algorithm
需要收到保护注意不要泄露。
此外:哈希函数将会返回很长的字符串,如下所示:
SELECT anon.hash('bob');
hash
--------------------------------------------
95b6accef02c5a725a8c9abf19ab5575f99ca3d9997984181e4b3f81d96cbca4d0977d694ac490350e01d0d213639909987ef52de8e44d6258d536c55e427397
对于某些列来说,这可能太长,你需要剪切哈希值的某些部分,以便放入字段中。
例如,电话号码的字段为18 位,你可以使用以下函数进行转换。
SECURITY LABEL FOR anon ON COLUMN people.phone_number
IS 'MASKED WITH FUNCTION anon.left(anon.hash(phone_number),12)';
SECURITY LABEL FOR anon ON COLUMN call_history.fk_phone_number
IS 'MASKED WITH FUNCTION anon.left(anon.hash(fk_phone_number),12)';
当然,如果将值剪切位12个字符时,会增加冲突的记录,请自行评估方案的可行性。
9 部分脱敏(Partial Scrambling)
部分脱敏将会部分数据进行脱敏。例如:信用卡号码可以替换为"40XX XXXX XXXX XX96"。
anon.partial('abcdefgh',1,'xxxx',3)
将会返回 'axxxxfgh'值;anon.partial_email('daamien@gmail.com')
将会返回' da**_*_*@gm******.com'
值。
select anon.partial('554221108910240067',1,'xxxxxxxxxxx',3);
//屏幕输出:
partial
-----------------
5xxxxxxxxxxx067
(1 row)
10 条件脱敏(Conditional Masking )
条件脱敏是指只对表中的某些数据,或某些行进行脱敏。
例如,如果你想要仅脱敏包含数值的数据,NULL值保留,可以在 CASE WHEN x THEN y ELSE z
语句中使用 anon.ternary
函数。
SECURITY LABEL FOR anon ON COLUMN player.score
IS 'MASKED WITH FUNCTION anon.ternary(score IS NULL,
NULL,
anon.random_int_between(0,100));
如果还要排除表中的某些行。比如保留某些用户的密码,以便他们仍能被应用程序所使用。
SECURITY LABEL FOR anon ON COLUMN account.password IS 'MASKED WITH FUNCTION anon.ternary( id > 1000, NULL::TEXT, password)';
条件脱敏会在原始数据和屏蔽数据之间建立一种关联性,例如,如果 deaders_date(死亡日期)
字段保留了NULL,就会显示那些人实际还活着。
11 泛化脱敏(Generalization)
泛化的原理是用一个值的区间范围替换原来的值。例如,“保罗42岁”,可以泛化为“保罗40 岁到50岁”。
泛化脱敏函数是一种数据类型转换,所以无法用于动态脱敏。不过他们在创建匿名视图时非常有用。
SELECT * FROM patient;
id | name | zipcode | birth | disease
----+----------+----------+------------+---------------
1 | Alice | 47678 | 1979-12-29 | Heart Disease
2 | Bob | 47678 | 1959-03-22 | Heart Disease
3 | Caroline | 47678 | 1988-07-22 | Heart Disease
4 | David | 47905 | 1997-03-04 | Flu
5 | Eleanor | 47909 | 1999-12-15 | Heart Disease
6 | Frank | 47906 | 1968-07-04 | Cancer
7 | Geri | 47605 | 1977-10-30 | Heart Disease
8 | Harry | 47673 | 1978-06-13 | Cancer
9 | Ingrid | 47607 | 1991-12-12 | Cancer
我们可以在表上建立一个视图,对邮政编码和出生日期进行泛化脱敏。
CREATE VIEW anonymized_patient AS
SELECT
'REDACTED' AS lastname,
anon.generalize_int4range(zipcode,100) AS zipcode,
anon.generalize_tsrange(birth,'decade') AS birth
disease
FROM patients;
脱敏之后的视图查询后应该是这样的。
SELECT * FROM anonymized_patient;
lastname | zipcode | birth | disease
----------+---------------+-----------------------------+---------------
REDACTED | [47600,47700) | ["1970-01-01","1980-01-01") | Heart Disease
REDACTED | [47600,47700) | ["1950-01-01","1960-01-01") | Heart Disease
REDACTED | [47600,47700) | ["1980-01-01","1990-01-01") | Heart Disease
REDACTED | [47900,48000) | ["1990-01-01","2000-01-01") | Flu
REDACTED | [47900,48000) | ["1990-01-01","2000-01-01") | Heart Disease
REDACTED | [47900,48000) | ["1960-01-01","1970-01-01") | Cancer
REDACTED | [47600,47700) | ["1970-01-01","1980-01-01") | Heart Disease
REDACTED | [47600,47700) | ["1970-01-01","1980-01-01") | Cancer
REDACTED | [47600,47700) | ["1990-01-01","2000-01-01") | Cancer
泛化脱敏后的值仍然有用,因为他仍然是真实的。但准确度非常低。
PostgreSQL 提供了多 RANGE 函数,非常适合日期和数值。
generalize_int4range(value, step)
generalize_int8range(value, step)
generalize_numrange(value, step)
- ...
value 是要泛化的数据,而step
为范围的大小。
12 使用 pg_catalog
函数
自从 1.3
版本起,pg_catalog 模式下的函数便设置为不可信。为了防止用户在 pg+
脱敏规则使用不应该作为脱敏函数的复杂函数,例如:pg_catalog.ts_stat 和 pg_catalog.ts_stat。
为了方便起见,此扩展将会和 pg_catalog 函数进行绑定使用。
- anon.concat(TEXT,TEXT)
- anon.concat(TEXT,TEXT, TEXT)
- anon.date_add(TIMESTAMP WITH TIME ZONE,INTERVAL)
- anon.date_part(TEXT,TIMESTAMP)
- anon.date_part(TEXT,INTERVAL)
- anon.date_subtract(TIMESTAMP WITH TIME ZONE, INTERVAL )
- anon.date_trunc(TEXT,TIMESTAMP)
- anon.date_trunc(TEXT,TIMESTAMP WITH TIME ZONE,TEXT)
- anon.date_trunc(TEXT,INTERVAL)
- anon.left(TEXT,INTEGER)
- anon.length(TEXT)
- anon.lower(TEXT)
- anon.make_date(INT,INT,INT )
- anon.make_time(INT,INT,DOUBLE PRECISION)
- anon.md5(TEXT)
- anon.random()
- anon.replace(TEXT,TEXT,TEXT)
- anon.regexp_replace(TEXT,TEXT,TEXT)
- anon.regexp_replace(TEXT,TEXT,TEXT,TEXT)
- anon.right(TEXT,INTEGER)
- anon.substr(TEXT,INTEGER)
- anon.substr(TEXT,INTEGER,INTEGER)
- anon.upper(TEXT)
如果你需要更多的绑定函数,你可以: - 自定义脱敏函数。
- 信任 pg_catalog ,具体请配置 anon.sourceshema 参数。
- 请在 GitLab 上提交 issue。
13 自定义脱敏函数
在这个小结您可以使用自己的函数作为脱敏函数。自定义脱敏函数必须是破坏性的(像 部分脱敏),或者在数据集中插入随机性(像伪造)。
自定义脱敏函数主要是针对复杂的数据类型,例如需要隐藏 JSON
字段的某些部分,以下是一个简单的例子。
CREATE TABLE company (
business_name TEXT,
info JSONB
)
信息字段包含JSON内容如下:
SELECT jsonb_pretty(info) FROM company WHERE business_name = 'Soylent Green';
//屏幕输出:
jsonb_pretty
----------------------------------
{
"employees": [
{
"lastName": "Doe",
"firstName": "John"
},
{
"lastName": "Smith",
"firstName": "Anna"
},
{
"lastName": "Jones",
"firstName": "Peter"
}
]
}
(1 row)
使用 PostgreSQL JSON functions and operators,将敏感数据进行脱敏。
CREATE SCHEMA custom_masks;
-- This step requires superuser privilege
SECURITY LABEL FOR anon ON SCHEMA custom_masks IS 'TRUSTED';
CREATE FUNCTION custom_masks.remove_last_name(j JSONB)
RETURNS JSONB
VOLATILE
LANGUAGE SQL
AS $func$
SELECT
json_build_object(
'employees' ,
array_agg(
jsonb_set(e ,'{lastName}', to_jsonb(anon.fake_last_name()))
)
)::JSONB
FROM jsonb_array_elements( j->'employees') e
$func$;
检查函数是否正确。
SELECT custom_masks.remove_last_name(info) FROM company;
如果验证成功使用此函数进行脱敏。
SECURITY LABEL FOR anon ON COLUMN company.info
IS 'MASKED WITH FUNCTION custom_masks.remove_last_name(info)';
查看脱敏后的数据
# SELECT anonymize_table('company');
# SELECT jsonb_pretty(info) FROM company WHERE business_name = 'Soylent Green';
jsonb_pretty
-------------------------------------
{
"employees": [ +
{ +
"lastName": "Prawdzik",+
"firstName": "John" +
}, +
{ +
"lastName": "Baltazor",+
"firstName": "Anna" +
}, +
{ +
"lastName": "Taylan", +
"firstName": "Peter" +
} +
] +
}
(1 row)
14 小结
这里还有一些其他脱敏函数,请查看具体适用场景。
PostgreSQL Anonymizer 脱敏函数 anon.anonymize_column
PostgreSQL Anonymizer 脱敏函数 anon.anonymize_table
PostgreSQL Anonymizer 脱敏函数 anon.shuffle_column
PostgreSQL 安全管理 数据脱敏 Anonymizer 泛化脱敏(Generalization)
PostgreSQL 安全管理 数据脱敏 Anonymizer 自动脱敏(Searching for Identifiers)