背景
很多行业对行业数据的存储都有自己的监管标准,例如:
- Payment Card Industry Data Security Standard (PCI DSS)
- Health Insurance Portability and Accountability Act (HIPAA)
- General Data Protection Regulation (GDPR)
- California Consumer Protection Act (CCPA)
- And more
这些监管标准中都要求数据必须是加密存储的。而为了对数据进行加密存储,首先想到的是文件级加密(FDE)。文件级加密虽然简单直接,但是依赖支持加解密接口的文件系统,或者是其他层面的加解密,例如云盘块存储层面。这对很多系统来说,代价比较高。很多商业数据库管理系统在数据库层面实现了TDE(Transparent Data Encryption),相对于FDE,优势如下:
- 不依赖文件系统
- 文件系统访问出的数据都是密文
- 可以选择加密某一列或者某个表从而降低性能影响
目前很多数据库已经实现了TDE,我们首先对各个数据库最后TDE 实现的效果有个大体的了解,然后再去探究PostgreSQL 社区当前对TDE 的讨论和一些结论。
MySQL (InnoDB)
MySQL 支持每个tablespace 数据级别的静态数据加密。MySQL 中tablespace 涉及到一个包含一个或者多个InnoDB 表和对应索引的文件(在PostgreSQL 中tablespace 对应一个目录)。 MySQL 8.0.16 支持了redo log 和undo log 的加密和系统表的加密,并且支持了2层加密体系,每个tablespace 在文件头中都存储着自己的密钥。主密钥可以通过keyring 插件来获取。
MySQL 加密redo log 和 undo log 使用和表数据加密不同的密钥。这些密钥存储在redo/undo log 加密后文件的头部。
Oracle DB
Oracle DB 支持列级别和tablespace 级别的TDE,都采用了二层加密的结构。主加密密钥(MEK)存放在额外的密钥存储中,支持软硬件的密钥存储。MEK 用来加密列级别和tablespace 级别的密钥。列级别每个表使用相同的加密密钥,tablespace 级别的每个tablespace 使用相同的加密密钥。支持3DES168 和AES (128,192,256 位)的加密算法。列级TDE 默认为AES-192算法,tablespace 级别的TDE 默认为AES-128 算法。在加密 之前,会在明文中增加一点额外信息(比如16字节的随机字符来保证相同明文加密得到不同的密文)。同时还会在密文上增加20字节的完整性检查值来保证密文的完整性,当然可以通过NOMAC 参数取消完整性检查值的填充来提高加密性能。
MS SQL Server
MS SQL Server 支持数据库级别的TDE,采用了三层的密钥加密构架,并支持对称和非对称加密算法。服务主密钥(Service Master Key 简称为SMK)在初始化数据的时候生成。数据库主密钥(Database Master Key,简称为DMK)在默认数据库中产生,并由SMK 加密存储。DMK 被用来生成证书来保证数据库加密密钥(Database Encryption Key,简称为DEK)的安全。DEK 使用对称加密算法对数据和日志文件进行加解密。
PostgreSQL
PostgreSQL 社区自2016年已经对TDE 展开了讨论,目前仍然存在很多的争论,但是确认了在PostgreSQL 13 的版本中会输出TDE 第一个版本,实现如下的功能:
- 集群级别的加密
- 数据库内部的密钥管理系统
- 加密持久化的所有,不加密内存中的数据
在这之前社区会对很多相关的问题进行讨论:
- 安全风险模型
- 加密的粒度
- 哪些文件需要加密
- 何时加密
- 如何加密
- 如何管理密钥
安全风险模型
在社区的讨论中,都一再强调一定要明确了TDE 要防护的安全风险模型之后,再去讨论具体的实现。目前基本明确TDE 保护突破了文件系统访问控制的安全威胁:
- 偷取存储设备,并且直接访问数据库文件的恶意用户
- 通过备份恢复数据的恶意用户
- 保护静态数据(持久化的数据)
加密的粒度
各个数据库在实现TDE 的时候加密的粒度都是不同的,当前加密的粒度可以分为:
- 数据库集群级别,例如PostgreSQL 社区2016 年邮件列表中的实现1 和PostgreSQL 社区2019 年邮件列表中 的实现2
- 数据库级别,例如SQL Server,详见链接
- 表级别,例如Oracle 和MySQL
- Tablespace 级别,例如MySQL,PostgreSQL 2019 年邮件列表中的提交的patch,但是目前社区对tablespace 级别的加密有很多反对的声音,觉得即复杂又没有太大意义,详见邮件列表
- 列级别,例如Oracle。PostgreSQL 社区认为这种更适合使用pgcrypto 插件,触发器和视图来实现。
- 定义新的范围,例如定义一组表支持加密
根据上文可知,PostgreSQL 13 第一个版本会实现集群级别的加密,这样的决定主要基于:
- 简单的结构,利于实现和扩展
- 适合于加密所有数据的需求 集群级别的加密已经满足TDE 需求中的一个,也满足了静态数据加密的标准。
当然,社区还是有其他不同的声音,认为更加细粒度的加密比集群级别的加密更具有优势:
- 减少不必要的性能开销
- 减少使用同一key 加密的数据
- 使得密文攻击更加困难
- 更加安全
- 重新加密不需要重建整个数据库集群
- 更利于多租户状态下的数据加密
当然这样也会引入其他问题,加密数据的表查找将会带来额外的开销。
哪些文件需要加密
哪些文件需要加密首先是取决于上文说的加密的粒度。在集群级别的加密中,它将会加密整个数据库集群数据包含所有的表,索引以及SLRU 数据。计划中的PostgreSQL 13 实现的第一个版本将会实现数据集群所有的数据同时也包含WAL 日志,临时文件等等,但是不加密内存中的数据。
另一方面,更细粒度级别的加密,数据库只加密部分数据,一般包含相关的:
- 表和索引
- WAL 日志
- 系统表记录
- 临时文件
在社区的计划中,目前对SLRU 数据,例如clog, commit timestamp, 2pc file 等等以及服务端日志pg_log 不需要进行加密,当然这个结论随着后面讨论的加深可能也会被推翻。另外,对prepared transaction 产生的持久化文件,目前Sawada-san 认为没有任何的重要数据,所以没有必要进行加密。社区目前将这部分作为一个todo list 等待未来继续讨论。
何时加密
大多数TDE 的实现都是数据在buffer 中的时候是明文,刷到磁盘上的时候进行加密,从磁盘上读取的时候进行解密,保证用于数据处理的时候是明文,而持久化到磁盘上的数据是密文。
不过在具体实现上,仍然有很多问题需要讨论。
- 在细粒度级别的加密中,WAL 日志的加密密钥是否要和表数据的相同?目前社区的讨论中第一个版本决定使用不同的密钥来加密WAL 日志。这样是更加符合静态数据加密的需求。但是,这样就必须要求backends 不能直接写WAL 日志,WAL 日志完全得交由独立的进程来刷盘。虽然当前已经有walwriter,但是不一定满足需求。
- 集群级别的加密,在使用pg_basebackup 拉取数据的同时可能需要支持更新密钥的需求,这样用户就很容易恢复出一个另外密钥的数据集群或者备库。
如何加密
社区中讨论了时下多种加密的算法,初步决定使用AES (Advanced Encryption Standard)对数据块进行加密。 对于AES 来说,有三个关键组成部分,包括算法/模式,密钥(key),初始向量(IV)。
AES 模式
计划使用AES 的CTR 模式对WAL 日志和表/索引数据进行加密。
密钥
计划使用密钥支持128 或者256位长度,可以在initdb 的时候选择密钥的长度。
初始向量
为了使得加密的密文随机化,AES 引入了IV。IV 要求每次加密必须是唯一的,但是不需要保密,对外可见。因为每次产生随机数来作为IV,开销比较大,我们可以用一些被加密对象的唯一值来作为IV。
- 使用(page LSN,page number) 来作为每个数据页的IV,page LSN 8个字节,page number 4个字节,通过补齐可以满足16字节IV 的需要
- 使用WAL 日志的段号来作为每个WAL 日志段的IV。每个WAL 日志文件使用不同的IV。 不过需要注意的是LSN 不需要加密,必须是可见的,才能保证IV 对外可见。同时也不会去加密CRC,在加密完成后计算CRC,这样就能保证pg_checksums 不通过解密数据依然能检查页面的完整性。
如何管理密钥
上文社区当前计划使用AES 的加密方法,所以本文接下来讨论的就是AES 的密钥如何来管理。一般情况下,为了减少密钥更换带来重解密重加密的开销,业界的方法都是采用多层密钥管理的结构,例如上文的Oracle,MySQL,MS SQL Server 等。其中Oracle,MySQL 使用的是两层密钥结构,MS SQL Server 使用的是三层密钥结构。
两层密钥结构
一般两层密钥结构中包含主密钥(master key,简称为MK)和 数据加密密钥(data encryption key,DEK)。MK 用来加密DEK,而且必须保存在数据库之外,例如KMS。DEK 用来加密数据库数据,可以使用MK 加密后保存在数据库本地。 如果我们使用单层密钥结构,更换密钥的时候必须要重新解密并加密。而使用两层密钥结构,我们需要重新加密和使用新MK 解密的只有DEK,更换密钥的速度会非常的快。
上文提到的PostgreSQL 2019 年邮件列表中的提交的tablespace 级别的TDE patch 中就使用两层密钥结构。
三层密钥结构
MS SQL Server 使用的就是三层密钥结构。但是在PostgreSQL 社区中讨论的结构可能略有不同,包含:
- 主密钥加密密钥(master key encryption key,简称为KEK):数据库管理员提供的密钥类似于ssl_passphrase_command 参数。
- 主数据加密密钥(master data encryption key,MDEK):是通过密码学安全随机数生成器生成的密钥,会被KEK 加密,按照一定的方式进行包装。
- 表数据加密密钥(Per table data encryption keys,TDEK)和WAL 数据加密密钥(WAL data encryption keys,WDEK):其中TDEK 是使用MDEK 和HKDF(HMAC-based Extract-and-Expand Key Derivation Function,基于HMAC 的提取和扩展密钥导出函数)生成出来加密表数据的密钥。WDEK 与之生成方法相同,不过是加密WAL 日志的密钥。
临时文件密钥
除了上文的两种密钥,社区认为临时文件的加密密钥需要单独拿出来讨论。临时文件密钥只在服务器运行过程中存在,可以只保存在内存中。在并发查询进行中,多个worker 是可以使用同一个临时文件的。所以临时文件的密钥需要多个并发查询worker 共享,其密钥的管理需要单独进行考虑。
如何拿到顶层密钥
无论是两层还是三层的密钥结构,需要更换的只有最顶层的密钥,而且该密钥必须要求存储在数据库之外。目前社区达成一致意见,增加一个类似ssl_passphrase_command 的参数来运行命令得到相应的密钥,例如上文中的支持集群级别的加密 提供了cluster_passphrase_command 参数。
不过,社区中支持tablespace 的patch 中实现了密钥管理的API 用来支持外部的密钥管理服务。
前端工具如何拿到密钥
一些前端工具如果需要直接读取数据库文件或者WAL 日志文件需要通过KMS 得到对应的顶层密钥,通过解密数据库中的密文得到加密数据的密钥,从而得到数据明文。
至此,我们已经对PostgreSQL 中实现TDE 的各个各个相关问题进行了讨论,但是目前很多具体的细节并未确认。不过基本确认的是PostgreSQL 13 会推出支持集群级别加密的TDE 功能,敬请期待。