> For the complete documentation index, see [llms.txt](https://freebsd-journal-cn.bsdcn.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://freebsd-journal-cn.bsdcn.org/20200304-wen-jian-xi-tong/openzfs-encryption-arrives-on-freebsd.md).

# OpenZFS 加密功能登陆 FreeBSD

OpenZFS GELI

OpenZFS on FreeBSD 新增的特性中，包含一个精巧的实现：在数据集层面采用行业标准加密。这意味着此前由 GELI 主导的 FreeBSD 存储加密生态迎来了新的竞争者：OpenZFS “原生加密”。本文从技术和可用性两个角度比较它与 GELI 的异同，帮助你判断哪种加密方案更适合自己（剧透：多数情况下是新方案）。

## GELI

GELI 一段时间以来是 FreeBSD 主要的本地全盘加密系统。GELI 使用 AES（通常是 AES-256，采用 XTS 或 CBC 模式，这两种模式特别适合磁盘数据加密），配合 SHA-256 哈希（与比特币所用的哈希相同）和消息认证。为此，GELI 借助 FreeBSD 的 **crypto(9)** 子系统提供硬件卸载，在支持 AES-NI 或其他加密加速的系统上（基本上是任何较新的平台）大幅提升性能。

然而，GELI 与 ZFS 毫无关系。GELI 加密的是整个设备，或者更细粒度地，设备上的分区。因此，首次使用任何以此方式保护的文件系统前，相关设备或分区必须“解锁”。解锁后，底层文件系统被识别并挂载，通常在整个运行期内保持。解锁过程由一个主密钥和至多两个用户密钥管理，后者涉及密钥文件和口令。这套复杂的密钥组合允许对 GELI 分区重新加密密钥（例如某个密钥泄露时）而不必重新加密数据。

FreeBSD 引导代码中集成了 GELI，因此只有一个小型 freebsd-boot 分区保持未加密，其中仅含足够代码用于提示输入 GELI 密钥口令、解密引导分区，并从底层 ZFS（或 UFS）文件系统读取引导加载程序或内核。这意味着整个文件系统（连同相关驱动器上每一字节数据）都被加密，只有那小块启动代码除外。

只要系统管理员正确实施，GELI 使用强加密、强模式、在数据密钥过度使用前轮换，并在实现中贯彻众多现代安全实践要素。因此 GELI 现在和将来都是一款在所有关键领域遵循最佳实践的安全加密实现。

OpenZFS 加密功能登陆 FreeBSD

常规块指针

```sh
+-------+-------+-------+-------+-------+-------+-------+-------+
|            vdev1              | GRID  |        ASIZE          |
+-------+-------+-------+-------+-------+-------+-------+-------+
|G|                       offset1                               |
+-------+-------+-------+-------+-------+-------+-------+-------+
|            vdev2              | GRID  |        ASIZE          |
+-------+-------+-------+-------+-------+-------+-------+-------+
|G|                      offset2                                |
+-------+-------+-------+-------+-------+-------+-------+-------+
|            vdev3              | GRID  |        ASIZE          |
+-------+-------+-------+-------+-------+-------+-------+-------+
|G|                      offset3                                |
+-------+-------+-------+-------+-------+-------+-------+-------+
|BDX|lvl| type  | cksum |E| comp|     PSIZE     |     LSIZE     |
+-------+-------+-------+-------+-------+-------+-------+-------+
|                        padding                                |
+-------+-------+-------+-------+-------+-------+-------+-------+
|                        padding                                |
+-------+-------+-------+-------+-------+-------+-------+-------+
|                        physical birth txg                     |
+-------+-------+-------+-------+-------+-------+-------+-------+
|                        logical birth txg                      |
+-------+-------+-------+-------+-------+-------+-------+-------+
|                        fill count                             |
+-------+-------+-------+-------+-------+-------+-------+-------+
|                        checksum[0]                            |
+-------+-------+-------+-------+-------+-------+-------+-------+
|                        checksum[1]                            |
+-------+-------+-------+-------+-------+-------+-------+-------+
|                        checksum[2]                            |
+-------+-------+-------+-------+-------+-------+-------+-------+
|                        checksum[3]                            |
+-------+-------+-------+-------+-------+-------+-------+-------+
/*
 * 图例：
 *
 * vdev         虚拟设备 ID
 * offset       虚拟设备中的偏移
 * LSIZE        逻辑大小
 * PSIZE        物理大小（压缩后）
 * ASIZE        已分配大小（包括 RAID-Z 校验和填充）
 * GRID         RAID-Z 布局信息（保留供将来使用）
 * cksum        校验和函数
 * comp         压缩函数
 * G            gang 块指示符
 * B            字节序（endianness）
 * D            去重
 * X            加密
 * E            blkptr_t 含嵌入式数据
 * lvl          间接层级
 * type         DMU 对象类型
 * phys birth   dva[0] 写入时的 txg；若与逻辑诞生 txg 相同则为零
 *              注意，通常所有 dva 都会在该 txg 中写入，
 *              但若被设备移除操作搬移则可能不同。
 * log. birth   块逻辑上诞生的交易组
 * fill count   此 bp 之下非零块的数量
 * checksum[4]  此 bp 描述的数据的 256 位校验和
 */
```

## OpenZFS 原生加密

到 2019 年年中，ZFS-on-Linux（ZoL）0.8 引入原生加密支持，即在受支持的 Linux 平台上对 ZFS 数据集本身加密。多亏 ZoL 和 FreeBSD 双方多位贡献者的努力，这项重要能力即将在 2020 年夏天前后随 FreeBSD 13 抵达 FreeBSD。

文件系统加密方案层面，我们不像 GELI 那样加密整个设备。这本身就很吸引人——常见的需求是只对敏感数据子集加密，而这些数据本来就可能自然隔离到独立数据集中！原生加密完全在数据集内运作，表现得就像又一个 ZFS 数据集属性。令人惊叹的是，ZFS 数据结构的基础布局无需扩展就能容纳加密！原生加密通过巧妙地重新利用 ZFS 块指针结构中的空闲空间实现。

加密块指针

```sh
+-------+-------+-------+-------+-------+-------+-------+-------+
|            vdev1              | GRID  |        ASIZE          |
+-------+-------+-------+-------+-------+-------+-------+-------+
|G|                       offset1                               |
+-------+-------+-------+-------+-------+-------+-------+-------+
|            vdev2              | GRID  |        ASIZE          |
+-------+-------+-------+-------+-------+-------+-------+-------+
|G|                       offset2                               |
+-------+-------+-------+-------+-------+-------+-------+-------+
|                         salt                                  |
+-------+-------+-------+-------+-------+-------+-------+-------+
|                         IV1                                   |
+-------+-------+-------+-------+-------+-------+-------+-------+
|BDX|lvl| type  | cksum |E| comp|     PSIZE     |     LSIZE     |
+-------+-------+-------+-------+-------+-------+-------+-------+
|                         padding                               |
+-------+-------+-------+-------+-------+-------+-------+-------+
|                         padding                               |
+-------+-------+-------+-------+-------+-------+-------+-------+
|                         physical birth txg                    |
+-------+-------+-------+-------+-------+-------+-------+-------+
|                         logical birth txg                     |
+-------+-------+-------+-------+-------+-------+-------+-------+
|               IV2             |          fill count           |
+-------+-------+-------+-------+-------+-------+-------+-------+
|                         checksum[0]                           |
+-------+-------+-------+-------+-------+-------+-------+-------+
|                         checksum[1]                           |
+-------+-------+-------+-------+-------+-------+-------+-------+
|                         MAC[0]                                |
+-------+-------+-------+-------+-------+-------+-------+-------+
|                         MAC[1]                                |
+-------+-------+-------+-------+-------+-------+-------+-------+
/*
 * 图例：
 *
 * salt         用于生成加密密钥的盐
 * IV1          96 位加密 IV 的前 64 位
 * X            块需要加密处理（设为 1）
 * E            blkptr_t 含嵌入式数据（设为 0，见下文）
 * fill count   此 bp 之下非零块的数量（截断为 32 位）
 * IV2          加密 IV 的最后 32 位
 * checksum[2]  此 bp 描述的数据的 128 位校验和
 * MAC[2]       此数据的 128 位消息认证码
 */
```

熟悉 ZFS 数据集属性的人想必遇到过参数 `copies`。它默认为 1，也可选 2（少见）或 3（几乎从不）。恰巧，数据结构中指向第三份副本（仅在几乎从不使用的 `copies=3` 情况下相关）的空间，恰好大小合适，可实现基于 AES 的加密策略。所以，以失去少用的三副本存储能力（外加校验！）为代价，我们换来了文件系统加密。与其他数据集属性一样，该属性由子数据集继承。事实上，加密属性与其他熟悉的 ZFS 数据集属性唯一显著的区别是，使用加密必须（理所当然地）在创建时设置，且不能取消。因此，数据集内的 ZFS 数据和元数据在挂载前以加密形式静止保存，但数据集的存在以及该数据集的 ZFS 属性（如 `logicalused` 以及其他可能揭示所含数据规模和范围的属性）必然始终对操作系统可见。

ZFS 原生加密最颠覆性的特性是：令人惊讶的是，数据集可在未挂载（因此仍处于加密状态）时进行 scrub 或 resilver。这一出人意料的能力通过将原本的 256 位哈希字段拆分为仍绰绰有余的 128 位哈希和 128 位消息认证码（MAC）实现。这意味着 ZFS 管理员与 GELI 情形不同，无需访问客户端解密密钥即可执行 ZFS 维护任务——这的确是个诱人的特性。

此外，敏感数据暂时无需访问时，数据集可以照例卸载，随后变为加密静止状态，无密钥者无法访问；而 GELI 通常在引导时解密整个文件系统并在整个运行期内保持解密，难以做到这一点。再者，GELI 整个文件系统由一组密钥保护，对不同用户的访问控制粒度很粗——用户要么能访问整个系统，要么什么都访问不了。原生 ZFS 数据集加密则可轻松为不同数据集配备不同密钥，并按合适的访问组合分发给指定用户。

引入 ZFS 原生加密的同时，ZFS send 也具备了发送“原始”数据块的能力，即数据块不被以任何方式解释，原样发送到接收方。对于原生加密数据集，这意味着无需加载密钥，数据块在网络上传输期间可保持加密，即便该传输（无意或有意地）本身未加密。这让敏感数据可以备份到远程 ZFS 系统，而远程系统或其管理员（以及前文所说的本地管理员）都永远无法访问明文。相比基于 GELI 的方案，这是显著的安全态势改进。

（注意：FreeBSD 引导代码尚不支持从加密数据集引导，但未来很可能成为可能！）

## 体验 OpenZFS 2.0

撰写本文时，OpenZFS 2.0 距离发布并合入 FreeBSD 源代码树大概还有几个月。等不及的话，可在 FreeBSD 12.1（及更高版本）上用 `sysutils/openzfs` 软件包安装预发布版。若不用 -RELEASE，最好从 Ports 编译 `sysutils/openzfs` 和 `sysutils/openzfs-kmod`（因为模块必须与将运行的内核精确匹配）。安装后，把 **/boot/loader.conf** 中的 `zfs_load="YES"` 替换为 `openzfs_load="YES"`。Port 或软件包会安装到标准 Ports 前缀 **/usr/local**，因此可能需要调整 `PATH` 环境变量，确保 shell 先找到 **/usr/local/sbin/zfs** 而非系统提供的 **/sbin/zfs**。一旦池中包含任何加密数据集，池本身（包括其未加密数据集）就再也无法在不支持加密特性标志的系统上导入。然而正如新 ZFS 特性常见的那样，一旦使用加密特性的最后一个数据集被销毁，加密特性标志会从“active”回退为“enabled”，池又能被旧版 ZFS 导入。提醒：用 OpenZFS 2.0 创建的池会启用新特性，可能不易向下兼容，因此此类实验应谨慎进行。`zpool` 手册页描述了如何在创建池时选择性地禁用单个特性标志。

## 把数据集迁移到 OpenZFS 原生加密

要从现有未加密数据集迁移数据（记住：GELI 加密的设备在文件系统层包含未加密数据集，因此也适用于 GELI 池！），先创建一个启用 OpenZFS 原生加密的新数据集：

```sh
zfs create -o encryption=aes-256-gcm -o keyformat=passphrase poolname/newdataset
```

然后，对原数据集创建快照，用 ZFS 复制把内容复制到新的加密数据集。完成后，可销毁原数据集，把加密数据集改回原名称，并更新挂载点。别忘了考虑由此产生的未分配空间的潜在安全影响——这些空间可能仍含未加密版本的数据；你可能想覆盖此空间。`zpool initialize` 命令是合理的实现方式。

若从 GELI 迁移，回忆一下 GELI 位于文件系统之前的层，因此不与文件系统交互。因此，移除 GELI 的唯一方法是让每个 GELI 加密的设备（或分区）重新以未加密设备的形式出现。所幸 ZFS 本身可以助我们一臂之力。例如，冗余度足够的池可以有策略地交替使用 `geli kill` 和 `zpool replace` 命令序列，逐一移除每个设备的 GELI 层。对于 ZFS 冗余度良好的池，这相对容易，可以安全地在线完成。对于冗余度不足或无冗余的池，此过程将需要（或更安全地配合使用）一块容量合适的临时辅助盘。

## 结论

GELI 很棒，陪伴我们多年。它加密整个设备（多数情况下包括其上的操作系统），例如你想保护被盗（或被没收）的笔记本或返修磁盘上的每一字节时，它无疑是上佳之选。然而，作为安全方案它的粒度很粗，而且让“已加密”数据在系统整个运行期内保持可访问状态存在相当大的风险。例如，黑客若获得 GELI 模式下挂载设备的端点访问权，原则上可全盘访问数据。

OpenZFS 原生加密则给管理员更大控制权——加密粒度更细，可灵活组合用户访问。它还允许未使用的加密数据集单独卸载，处于受保护的“静止”状态，而系统其余部分仍可访问。对于存放敏感数据、运行时间可能长达数月甚至数年的文件服务器而言，这比 GELI 更合理。若黑客获得不受限的端点访问权（物理或网络），他在数据集未挂载时看到的内容，与无特权用户看到的一样。OpenZFS 原生加密目前唯一的缺点是必然泄露其所加密数据性质的部分信息：数据集是否存在、数据集名称、任何快照的名称、数据量、可能的创建时间、可压缩程度等等。通常而言，这类信息无关紧要——某台服务器存放例如 4000 名患者的健康记录或监控视频，这本身并不敏感，但这些记录和视频的实际内容才敏感。同样难以反驳的是，任何要求更少系统管理员（相对于内容所有者）能访问数据的加密方案都更可取。

很难想象除上述被盗笔记本场景外，还有什么场景下 GELI 像 OpenZFS 原生加密那样合理、好用、可扩展。GELI 作为设备级加密方案当然会继续作为可信可靠的解决方案陪伴我们，但粒度精细到单个数据集的基于 ZFS 的加密方案极具吸引力，很可能成为今后 ZFS 用户期望的标配。

***

**ALLAN JUDE** 是 Klara Inc. 的工程副总裁，Klara Inc. 是一家全球性的 FreeBSD 专业服务与支持公司。他还主持每周一期的重要 BSD 播客 BSDNow\.tv，并于 2016 和 2018 年当选 FreeBSD 核心团队。他与 Michael W Lucas 合著了《FreeBSD Mastery: ZFS》和《FreeBSD Mastery: Advanced ZFS》。

**KYLE “DRKK” KNEISL** 拥有数学博士学位，在华盛顿特区担任科学家。他自 2013 年起是 FreeBSD 和 FreeNAS 社区的活跃成员与倡导者。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://freebsd-journal-cn.bsdcn.org/20200304-wen-jian-xi-tong/openzfs-encryption-arrives-on-freebsd.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
