PostgreSQL码农集散地

column_encrypt: PG 推出列级加密插件

column_encrypt: 列级加密“应用无感、密钥可轮换、SQL 可继续写”

https://github.com/vibhorkum/column_encrypt

很多企业说“数据库要加密”,其实说的不是一个问题,而是一组互相牵制的问题:

  • 数据落盘后不能是明文。
  • 应用最好不要大改 SQL。
  • DBA 最好不能随便看到敏感字段。
  • 密钥不能明文放数据库里。
  • 将来要能轮换密钥。
  • 还要能查等值条件,比如手机号、身份证、邮箱。
  • 逻辑复制、备份恢复、审计日志都不能把明文泄漏出去。

这几条放在一起,就会发现:列级加密不是“调用一下 AES 函数”这么简单。

column_encrypt 这个 PostgreSQL 扩展的定位,是提供透明列级加密。它提供 encrypted_text 和 encrypted_bytea 两个自定义类型,在类型输入/输出层完成加密和解密,让应用在插入、更新、查询时尽量保持原来的 SQL 形态。

先说结论:

column_encrypt 真正解决的不是“有没有 AES”,而是把列级加密需要的类型封装、密钥加载、版本头、密钥轮换、日志脱敏、权限控制、逻辑复制边界和等值查询模式,打包成一套 PostgreSQL 内部可运行的工程方案。

一、背景:为什么列级加密越来越刚需?

PostgreSQL 本身可以做很多层面的安全防护:

  • 网络层 TLS。
  • 磁盘层 LUKS、云盘加密、文件系统加密。
  • 权限层 GRANT、RLS、schema 隔离。
  • 审计层 pgaudit、日志审计。
  • 备份层加密。
  • 应用层自行加密敏感字段。

这些方案各有价值,但都不能完全替代列级加密。

原因很简单:敏感数据的风险不是只发生在磁盘丢失时。

真实生产里,泄漏路径包括:

  • DBA 或运维直接查询敏感字段。
  • 备份文件被复制走。
  • 逻辑复制链路把明文带到下游。
  • 测试环境使用了生产数据。
  • SQL 日志记录了 INSERT/UPDATE 明文。
  • 应用 bug 把不该返回的字段返回了。
  • 数据库超级权限、运维脚本、ETL 任务扩大了明文暴露面。

如果只做磁盘加密,数据库进程读出来仍然是明文;如果只靠权限控制,高权限账号仍然能看到;如果只靠应用层加密,应用改造成本和密钥治理成本都不低。

所以列级加密的核心目标是:

敏感列在数据库存储层以密文存在,只有持有正确密钥的会话才能透明读写明文。

column_encrypt 就是沿着这个方向设计的。

二、痛点:传统 PostgreSQL 敏感字段保护方案各有短板

1. 磁盘加密保护不了数据库内部明文

云盘加密、LUKS、TDE 类方案主要解决“存储介质丢失”问题。

它们能防:

  • 磁盘被拔走。
  • 快照文件泄漏。
  • 底层块设备被复制。

但防不了:

  • 数据库超级用户查询。
  • 应用误查。
  • SQL 日志明文。
  • 逻辑复制输出明文。
  • 备份逻辑导出明文。

因为 PostgreSQL 进程看到的数据仍然是明文。

2. 权限控制保护不了高权限路径

你可以不给普通用户访问敏感列,也可以用 view、RLS、函数封装。

但问题是:

  • 超级用户天然绕过很多权限边界。
  • DBA 常常需要排障权限。
  • ETL、BI、审计账号容易被授权过宽。
  • 权限模型会随着业务增长变复杂。

权限控制是必要的,但不是加密。

3. 应用层加密改造成本高

应用层加密的安全边界最清晰:数据库只存密文,密钥不进数据库。

但代价也明显:

  • 每个应用都要改加解密逻辑。
  • 多语言、多服务实现容易不一致。
  • SQL 查询能力下降。
  • 历史数据迁移麻烦。
  • 密钥轮换要应用和数据层协同。
  • 字段类型、ORM、序列化、索引策略都要改。

如果只是少数字段还好;如果是多个业务系统、多个表、多个团队,工程复杂度会迅速上升。

4. 手写 pgcrypto 函数容易变成“半成品加密”

PostgreSQL 有 pgcrypto,可以自己写:

INSERTINTO t(ssn) VALUES (pgp_sym_encrypt('888-999-2045', 'passphrase'));  

SELECT pgp_sym_decrypt(ssn, 'passphrase') FROM t;  

但这样会引出一串问题:

  • 应用 SQL 要改。
  • 密钥容易出现在 SQL 文本、日志、连接池、审计里。
  • 不同字段、不同表的封装难统一。
  • 密钥版本怎么记录?
  • 轮换怎么批量重加密?
  • 等值查询怎么做?
  • 权限怎么收口?
  • 错误处理怎么规范?

pgcrypto 是密码学工具箱,不是完整列级加密产品。

三、column_encrypt 的方案:自定义类型 + 会话密钥 + 版本化密文

column_encrypt 的核心设计是两个自定义 base type:

  • encrypted_text
  • encrypted_bytea

它们底层使用变长 bytea 存储。

关键点在于类型 I/O:

  • INSERT / UPDATE 时,类型 input function 自动把明文加密成密文。
  • SELECT 时,类型 output function 自动把密文解密成明文。
  • 应用 SQL 可以继续像写普通 text 一样写入值。
  • 前提是当前 session 已经加载了正确密钥。

这相当于把加解密逻辑放进 PostgreSQL 类型系统。

示意:

应用 SQL: INSERT INTO secure_data(ssn) VALUES('888-999-2045')  
        |  
        v  
encrypted_text input function  
        |  
        v  
使用 session 中加载的 DEK 加密  
        |  
        v  
存储: key version header + ciphertext  

SELECT ssn FROM secure_data  
        |  
        v  
encrypted_text output function  
        |  
        v  
根据 ciphertext header 找 key version  
        |  
        v  
使用 session keyring 解密并输出明文  

这个设计的产品价值是:

应用看到的是类似普通列的使用体验,数据库里存的是带 key version header 的密文。

四、两层密钥模型:KEK 包 DEK,DEK 不明文落库

README 中明确说明 column_encrypt 使用两层密钥:

密钥
作用
存储方式
DEK, Data Encryption Key
加密/解密列数据
被 KEK 包裹后存入 cipher_key_table,会话加载后进入内存
KEK, Key Encryption Key / Master Passphrase
包裹 DEK
不存入数据库,由外部管理

注册密钥时:

SELECT encrypt.register_key('my-secret-data-key', 'my-master-passphrase');  

它会用 master passphrase 通过 pgcrypto 的 pgp_sym_encrypt 包裹 DEK。README 说明使用 AES-256/S2K:

cipher-algo=aes256, s2k-mode=3  

使用时,每个 session 需要加载 key:

SELECT encrypt.load_key('my-master-passphrase');  

加载后,DEK 存在 backend 的 C session memory 中。README 说明 keys 保存在 TopMemoryContext 的 versioned in-memory keyring,卸载时用 secure_memset 清零。

卸载:

SELECT encrypt.unload_key();  

这个模型的优点是:

  • 数据库表里不存明文 DEK。
  • 应用 session 必须持有 passphrase 才能解密。
  • 同一 session 可以加载多个 key version,支持轮换。
  • key version 和 ciphertext 绑定,便于历史数据解密和重加密。

五、密文头:2 字节 key version 是轮换的基础设施

列级加密最容易被低估的问题是 key rotation。

如果密文里没有版本信息,几年后你会遇到一个麻烦:

这行数据到底是用哪把 key 加密的?

column_encrypt 在每个 ciphertext 前加 2 字节 key version header。

README 中的格式:

  • bit 15,也就是 0x8000,设置为 flag。
  • low 15 bits 存 key version。
  • key version 范围 1-32767。
  • 网络字节序,保证跨平台兼容。
  • 旧格式数据可 fallback 自动识别。

这看似只是 2 字节,但工程意义很大:

  • 解密时能按 header 找对应版本 key。
  • 轮换时可以知道哪些行还没重加密。
  • 多版本 keyring 可以平滑过渡。
  • 将来审计、验证、迁移都有依据。

没有版本头的加密,通常后期都会在轮换上还债。

六、安全模型:不是只加密,还要收住执行权限和日志

1. 单一角色 column_encrypt_user

README 中 v4.0 采用统一角色:

GRANT column_encrypt_user TO app_user;  
GRANT column_encrypt_user TO key_manager;  

所有 encrypt.* 函数都是 SECURITY DEFINER,并且从 PUBLIC revoke。直接访问 cipher_key_table 也从 PUBLIC revoke。

这让权限边界更清楚:

  • 不是所有数据库用户都能注册、加载、轮换密钥。
  • 应用角色需要显式获得 column_encrypt_user。
  • 密钥管理函数不暴露给 PUBLIC。

2. encrypt.enable 是 superuser-only

encrypt.enable 是 PGC_SUSET,只有 superuser 能改。

这点很关键。否则普通用户如果能关闭 encryption,就可能绕过输入/输出路径的安全逻辑。

3. 自动日志脱敏

column_encrypt 使用 emit_log_hook 对敏感 key-management function call 做日志脱敏。

相关 GUC:

encrypt.mask_key_log = on  

这是 defense-in-depth,防止注册 key、加载 key 时 passphrase 出现在 PostgreSQL 日志里。

但 README 也特别提醒:默认情况下,普通 INSERT/UPDATE 里的 plaintext values 仍可能出现在 PostgreSQL 日志里。

生产建议开启:

SET encrypt.mask_query_literals = on;  

或全局配置:

encrypt.mask_query_literals = on  

效果是把字符串 literal 脱敏,例如:

INSERTINTO t(ssn) VALUES('123-45-6789')  

日志里变成:

INSERTINTO t(ssn) VALUES('***')  

代价是所有 string literal 都被遮蔽,日志可观测性下降。

这是典型安全权衡:

生产敏感数据系统,日志可读性要让位于明文泄漏风险。

4. 禁止 binary protocol SEND/RECEIVE

README 明确说 binary protocol SEND / RECEIVE 被有意拒绝,避免客户端绕过 text I/O 加密路径。

这是一个容易被忽略但很重要的边界。

如果自定义类型只在 text I/O 做加解密,而 binary I/O 可以直接传底层 bytes,就可能出现绕过。这个扩展选择直接拒绝 binary protocol,是更保守的安全设计。

七、等值、哈希和索引:加密列最难的是“还能不能查”

加密字段一旦进入数据库,就会遇到查询问题。

1. equality/hash 基于解密明文

README 说明 equality 和 hash 语义定义在 decrypted plaintext 上,而不是 raw ciphertext bytes 上。

这解决一个关键问题:

同一个明文经过不同 key version 加密后,ciphertext 不同,但逻辑上应该相等。

如果比较 raw ciphertext,密钥轮换后同一值会失去相等语义。
如果比较 decrypted plaintext,相等判断在逻辑上是正确的。

但是代价也明显:

  • 比较需要可用 session key。
  • 大规模索引等值查找不适合直接依赖解密比较。
  • hash index 语义曾经变化过,README 提醒从 v2.x 升级后要重建 hash index。

2. range ordering 不支持

README 明确说 range ordering on encrypted values intentionally unsupported。

也就是不要指望:

WHERE encrypted_col > 'abc'  
ORDER BY encrypted_col  

这是正确取舍。
如果加密后还想保留范围顺序,通常意味着使用 order-preserving / order-revealing encryption,这会泄漏顺序信息,安全模型完全不同。

column_encrypt 选择不支持范围排序,是偏安全和明确边界的设计。

3. 推荐 blind index 做可扩展等值查询

README 推荐 companion blind-index column。

示例:

ALTERTABLE secure_data ADDCOLUMN ssn_blind_index text;  

UPDATE secure_data  
SET ssn_blind_index = encrypt.blind_index('888-999-2045', 'blind-index-secret');  

更实际的表设计应该是:

CREATETABLE secure_data (  
id bigserial PRIMARY KEY,  
    ssn encrypt.encrypted_text,  
    ssn_bidx text
);  

CREATEINDEXON secure_data(ssn_bidx);  

插入时同时写:

INSERTINTO secure_data(ssn, ssn_bidx)  
VALUES (  
'888-999-2045',  
    encrypt.blind_index('888-999-2045', 'blind-index-secret')  
);  

查询时:

SELECTid, ssn  
FROM secure_data  
WHERE ssn_bidx = encrypt.blind_index('888-999-2045', 'blind-index-secret');  

这里的思想是:

  • 加密列负责保密存储。
  • blind index 负责等值定位。
  • blind-index key 要和 DEK/KEK 分开管理。

注意:blind index 适合 equality lookup,不适合范围查询,也会泄漏相同值出现的频率。比如同一个手机号出现多次,blind index 也会相同。

八、密钥轮换:这个扩展的工程成熟度主要看这里

README 给出的轮换流程:

-- Step 1: Register a new key version (inactive by default)  
SELECT encrypt.register_key('new-data-key', 'my-master-passphrase', false);  

-- Step 2: Load all key versions for rotation  
SELECT encrypt.load_key('my-master-passphrase', all_versions => true);  

-- Step 3: Activate the new version and re-encrypt  
SELECT encrypt.activate_key(2);  
SELECT encrypt.rotate('public', 'secure_data', 'ssn');  

-- For large tables, use a smaller batch_size  
SELECT encrypt.rotate('public', 'secure_data', 'ssn', 5000);  

-- Step 4: Clear the session keyring  
SELECT encrypt.unload_key();  

这里要看懂几个关键点:

1. 新 key 可以先注册为 inactive

SELECT encrypt.register_key('new-data-key', 'my-master-passphrase', false);  

这允许你先准备好 key,不立即影响新写入。

2. 轮换时要加载所有版本

SELECT encrypt.load_key('my-master-passphrase', all_versions => true);  

因为旧数据需要用旧 key 解密,再用新 active key 加密。

3. activate_key() 设置 active key

SELECT encrypt.activate_key(2);  

README 说明它会设置 encrypt.key_version,让后续新加密写入带新版本。

4. rotate() 做整列重加密

SELECT encrypt.rotate('public', 'secure_data', 'ssn', 5000);  

batch_size 控制内部 UPDATE chunk size,降低单次 UPDATE 锁持有时间。

但要注意:README 也说 rotate 仍是在一次函数调用里处理整个 column,batch_size 是内部 chunk。

生产大表轮换要额外关注:

  • 表膨胀。
  • WAL 放大。
  • 复制延迟。
  • autovacuum 压力。
  • 锁等待。
  • 业务低峰窗口。
  • 回滚风险。

5. 轮换后卸载 key

SELECT encrypt.unload_key();  

这会清除 session memory 中的 keyring。

九、逻辑复制:复制密文,不复制明文

README 对逻辑复制的边界讲得很明确:

column_encrypt replicates ciphertext, not plaintext。

这意味着:

  • subscriber 也要安装 extension。
  • subscriber 独立管理和加载 keys。
  • cipher_key_table 中 wrapped keys 不会变成 subscriber 可用 session keys。
  • 复制/apply role 应该设置 encrypt.enable = off,让复制传输 ciphertext,而不是 decrypted plaintext。

推荐模式:

ALTERROLE replication_user SET encrypt.enable = off;  
ALTERROLE subscription_owner SET encrypt.enable = off;  

普通应用 session 仍保持:

encrypt.enable = on  

并只在需要读写明文的角色中加载 key。

这个设计符合安全直觉:

复制链路应该搬运密文,不应该因为下游同步而扩大明文暴露面。

项目也提供 Docker 逻辑复制 harness:

./run-docker-logical-replication.sh 18  

十、效果对比:column_encrypt 改变的是改造成本和密钥治理方式

维度
传统 pgcrypto 手写
应用层加密
磁盘/云盘加密
column_encrypt
数据库存储
可密文
密文
数据库内仍明文
密文
应用 SQL 改造
大
大
无
较小
密钥是否进数据库
容易误进 SQL/日志
通常不进
不适用
KEK 不存库,DEK wrapped 存储
session 无 key 查询
取决于实现
应用控制
可查明文
报错
密钥版本
自己设计
自己设计
不适用
ciphertext header 内置
轮换
自己写脚本
应用协同
存储层轮换
activate_key
 + rotate
日志脱敏
自己处理
应用处理
不能处理 SQL 明文
key 调用自动遮蔽,可选 literal masking
等值查询
自己设计
自己设计
原生可查但明文
推荐 blind index
范围查询
可做但有泄漏风险
取决于设计
原生
不支持
逻辑复制
自己约束
密文
可能复制明文
推荐复制密文

一句话:

column_encrypt 的价值不是发明加密算法,而是把 PostgreSQL 列级加密的工程坑集中收口。

十一、适合哪些场景?

场景 1:PII 字段保护

典型字段:

  • 身份证号。
  • 手机号。
  • 邮箱。
  • 社保号。
  • 护照号。
  • 银行卡号。
  • 地址。

表设计:

CREATETABLE customer_profile (  
id bigserial PRIMARY KEY,  
nametext,  
    mobile encrypt.encrypted_text,  
    mobile_bidx text,  
    id_card encrypt.encrypted_text,  
    id_card_bidx text,  
    created_at timestamptz DEFAULTnow()  
);  

CREATEINDEXON customer_profile(mobile_bidx);  
CREATEINDEXON customer_profile(id_card_bidx);  

写入:

SELECT encrypt.load_key('my-master-passphrase');  

INSERTINTO customer_profile(name, mobile, mobile_bidx, id_card, id_card_bidx)  
VALUES (  
'alice',  
'13800000000',  
    encrypt.blind_index('13800000000', 'mobile-bidx-secret'),  
'110101199001010000',  
    encrypt.blind_index('110101199001010000', 'idcard-bidx-secret')  
);  

等值查询:

SELECTid, name, mobile, id_card  
FROM customer_profile  
WHERE mobile_bidx = encrypt.blind_index('13800000000', 'mobile-bidx-secret');  

场景 2:多应用共享数据库,但只有部分角色可看明文

给需要读写敏感字段的应用角色授权:

GRANT column_encrypt_user TO app_sensitive;  

不给普通分析角色授权,也不让其加载 key。
即使它能查询表,读 encrypted column 也会因为没有 session key 报错。

测试:

SELECT * FROM secure_data;  

预期:

ERROR: cannot decrypt data, because key was not set  

场景 3:生产数据同步到下游,但下游默认不掌握 key

逻辑复制只传 ciphertext。
下游如果只是做备份、灾备、冷数据归档,不加载 key,就不能读明文。

复制角色配置:

ALTERROLE repl_user SET encrypt.enable = off;  

下游需要实际解密时,再由被授权角色加载 passphrase。

场景 4:合规要求定期轮换密钥

轮换步骤:

SELECT encrypt.register_key('new-data-key', 'my-master-passphrase', false);  
SELECT encrypt.load_key('my-master-passphrase', all_versions => true);  
SELECT encrypt.activate_key(2);  
SELECT encrypt.rotate('public', 'secure_data', 'ssn', 5000);  
SELECT encrypt.unload_key();  

轮换后验证:

SELECT * FROM encrypt.keys();  
SELECT encrypt.loaded_cipher_key_versions();  
SELECT * FROM encrypt.status();  

抽样验证:

SELECT *  
FROM encrypt.verify('public', 'secure_data', 'ssn', 100);  

十二、实操:从安装到第一张加密表

1. 安装依赖

要求:

  • PostgreSQL 14+。
  • pgcrypto。
  • C compiler。
  • PostgreSQL development headers。

pgcrypto 在 extension control 中作为 dependency,创建扩展时会自动安装。

2. 编译安装

git clone https://github.com/vibhorkum/column_encrypt.git  
cd column_encrypt  

export PATH=/usr/pgsql-<version>/bin:$PATH

make  
make install  

3. 配置预加载

修改 postgresql.conf:

shared_preload_libraries = '$libdir/column_encrypt'  

重启:

pg_ctl restart -D $PGDATA

或:

systemctl restart postgresql  

4. 创建 schema 和 extension

先创建 schema:

CREATESCHEMAIFNOTEXISTSencrypt;  

再创建扩展:

CREATE EXTENSION column_encrypt;  

5. 设置 search_path

推荐数据库级:

ALTERDATABASE your_database SET search_path TOpublic, encrypt, pg_catalog;  

或角色级:

ALTERROLE your_role SET search_path TOpublic, encrypt, pg_catalog;  

也可以显式使用 schema-qualified type:

encrypt.encrypted_text  

6. 授权应用角色

GRANT column_encrypt_user TO app_user;  
GRANT column_encrypt_user TO key_manager;  

7. 注册 key

用有 column_encrypt_user 的角色执行:

SELECT encrypt.register_key('my-secret-data-key', 'my-master-passphrase');  

8. 每个 session 加载 key

SELECT encrypt.load_key('my-master-passphrase');  

可以查看当前 session 加载了哪些 key version:

SELECT encrypt.loaded_cipher_key_versions();  

9. 建表并写入

CREATETABLE secure_data (  
idserial PRIMARY KEY,  
    ssn encrypt.encrypted_text  
);  

INSERTINTO secure_data(ssn) VALUES('888-999-2045');  
INSERTINTO secure_data(ssn) VALUES('888-999-2046');  
INSERTINTO secure_data(ssn) VALUES('888-999-2047');  

读取:

SELECT * FROM secure_data;  

预期:

 id |     ssn  
----+--------------  
  1 | 888-999-2045  
  2 | 888-999-2046  
  3 | 888-999-2047  

10. 新 session 不加载 key 的行为

SELECT * FROM secure_data;  

预期:

ERROR: cannot decrypt data, because key was not set  

错误 passphrase:

SELECT encrypt.load_key('wrong-passphrase');  

预期:

ERROR: incorrect passphrase  

11. 生产建议开启 literal masking

encrypt.mask_query_literals = on  

或者 session 级:

SET encrypt.mask_query_literals = on;  

注意:这些 GUC 都是 superuser 才能改。

十三、GUC 参数怎么理解?

参数
默认值
作用
建议
encrypt.enableon
全局启用/禁用列加密
普通应用保持 on,复制角色可按 README 建议 off
encrypt.mask_key_logon
遮蔽敏感 key-management 调用日志
生产保持 on
encrypt.mask_query_literalsoff
遮蔽所有 SQL string literal
敏感生产建议 on,但接受日志可观测性下降
encrypt.key_version1
写入 ciphertext header 的 key version
通过 activate_key() 管理,不建议手工乱改

所有参数都是 PGC_SUSET,需要 superuser 修改。

十四、升级注意事项

README 中强调 v4.0 移除了 deprecated functions。升级到 v4.0 要先经过 v3.3:

ALTER EXTENSION column_encrypt UPDATETO'3.3';  

然后按 MIGRATION.md 更新代码到 encrypt.* API,再升级:

ALTER EXTENSION column_encrypt UPDATETO'4.0';  

从 v2.x 升级后,如果已经在 encrypted_text 或 encrypted_bytea 上建过 hash index,需要重建。

原因是 equality/hash 语义已转向 decrypted plaintext,不再按 ciphertext bytes 处理。

十五、测试与验证

1. Docker regression

如果不想在主机安装开发包:

./run-docker-regression.sh  

指定版本:

./run-docker-regression.sh 14  
./run-docker-regression.sh 15  
./run-docker-regression.sh 16  
./run-docker-regression.sh 17  
./run-docker-regression.sh 18  

它会:

  • 构建临时 Ubuntu 测试镜像。
  • 安装对应 PostgreSQL server/contrib/dev packages。
  • 执行 make 和 make install。
  • 初始化临时 cluster。
  • 设置 shared_preload_libraries = 'column_encrypt'。
  • 创建测试库并运行 make installcheck。

2. 逻辑复制 harness

./run-docker-logical-replication.sh 18  

用于验证 publisher/subscriber 的 ciphertext 复制模式。

3. 上线前验收清单

建议至少验证:

  • 无 key session 读取 encrypted column 是否失败。
  • 正确 passphrase 是否能 load key。
  • 错误 passphrase 是否返回 28P01。
  • INSERT/UPDATE 是否实际落密文。
  • SQL 日志是否按预期脱敏。
  • blind index 查询是否走索引。
  • key rotation 后旧数据是否可读。
  • rotate 期间 WAL、锁、复制延迟是否可接受。
  • logical replication 是否复制 ciphertext。
  • 备份恢复后 key 管理流程是否仍然可用。

十六、边界条件:不要把列级加密想成魔法

1. session 加载 key 后,查询结果就是明文

只要 session 加载了 key,SELECT 输出就是明文。
所以仍然要控制:

  • 谁能获得 column_encrypt_user。
  • 谁知道 passphrase。
  • 应用是否会把明文打日志。
  • BI/ETL 是否误读敏感列。

列级加密降低存储和非授权 session 的暴露面,不等于消灭所有明文路径。

2. passphrase 管理仍然在数据库外

README 明确 KEK/master passphrase 不存数据库。
这意味着你必须有外部密钥管理方案,例如:

  • KMS。
  • Vault。
  • 密钥注入平台。
  • 运维审批流程。
  • 应用启动时安全加载。

扩展不替你解决组织层面的密钥治理。

3. blind index 会泄漏等值频率

blind index 可以查等值,但相同明文会有相同 blind index。

攻击者如果能看到 blind index 分布,可能推断频率模式。
对低基数字段,例如性别、状态、地区,不适合 blind index。

4. 不支持范围查询是有意为之

不要试图在 encrypted column 上做 <、>、ORDER BY。
如果业务强依赖范围查询,应该重新设计数据模型,或者接受更复杂且有泄漏风险的专门加密方案。

5. 超级用户仍是强信任主体

PostgreSQL extension 运行在数据库进程内。超级用户、主机 root、能调试进程的人,仍然属于强信任边界。

这类方案主要降低数据库表、备份、复制、普通账号、误操作日志中的明文暴露,不是硬件安全模块级别的隔离。

十七、结论

column_encrypt 是一个围绕 PostgreSQL 类型系统设计的透明列级加密扩展。

它把敏感字段保护拆成几件工程上必须同时成立的事:

  • 用 encrypted_text、encrypted_bytea 封装列类型。
  • 用 type input/output 做透明加解密。
  • 用 KEK/DEK 两层模型避免 DEK 明文落库。
  • 用 2 字节 key version header 支撑未来密钥轮换。
  • 用 session keyring 控制谁能读明文。
  • 用 SECURITY DEFINER 和 column_encrypt_user 收口权限。
  • 用 emit_log_hook 和 literal masking 降低日志泄漏。
  • 用 blind index 支撑可扩展等值查询。
  • 用 rotate/verify/status/keys 等函数补齐生命周期管理。
  • 用 ciphertext replication 模式控制逻辑复制明文扩散。

它最适合的场景是:

  • 需要保护 PII、证件号、手机号、邮箱、地址等敏感字段。
  • 应用希望尽量少改 SQL。
  • 数据库需要存密文,但授权 session 可以透明读写。
  • 合规要求支持密钥轮换。
  • 下游复制、备份、测试环境不应默认持有明文。

它不适合的场景是:

  • 强依赖加密字段范围查询。
  • 无法接受 session 加载 key 后输出明文。
  • 没有外部 passphrase/KMS 管理能力。
  • 希望靠一个扩展解决所有权限、审计、应用泄漏问题。

最后一句话:

列级加密的难点不是 AES,而是密钥生命周期、SQL 改造成本、查询能力、安全边界和运维流程。column_encrypt 的价值,就是把这些工程问题尽量放进 PostgreSQL 的类型、函数、GUC 和角色体系里,让敏感列从“靠约定保护”变成“靠机制保护”。