> For the complete documentation index, see [llms.txt](https://handbook.bsdcn.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://handbook.bsdcn.org/di-23-zhang-z-wen-jian-xi-tong-zfs/23.6.-zfs-yuan-sheng-jia-mi.md).

# 23.6.ZFS 原生加密

ZFS 支持对数据集及其中存储的数据进行原生加密。早期方案并非如此，此前的解决方案依赖基于 FreeBSD GELI 的加密。虽然这种方式安全，但不易移植到不支持 GELI 的非 FreeBSD 系统。借助 ZFS 原生加密，加密数据集可以在其他支持此池特性的系统上使用，无需依赖第三方组件或操作系统原生加密。

ZFS 原生加密的另一优势在于，执行快照、复制或 scrub 等管理任务时无需解密数据集。ZFS 数据完整性校验在原生加密数据集上同样有效，压缩也能在加密数据集上工作。

同一池中的不同数据集可以独立使用各自的密钥。考虑一个中央文件服务器，不同用户将其数据存储在加密的 home 数据集中。用户 A 与用户 B 使用不同的密钥。两位用户可以并行工作，按需解密自己的数据集，且无需知晓对方的解密密钥。各用户的解密方式也可以完全不同，互不影响。这样一来，不同用户的敏感数据可以存放在同一池中，同时仍由各自的密钥保护。

使用 ZFS 原生加密前，请注意以下事项：

* 加密在数据集级别应用，而非池级别。
* FreeBSD 的 loader 暂不支持从加密 ZFS 数据集引导。
* 加密只能在创建数据集时启用，事后无法启用。
* 块加密算法、密钥长度和加密模式一经设置便无法更改。
* ZFS 不加密元数据。

最后一点相比全盘加密似乎是个缺点。然而，它提供了更大的灵活性，允许使用不同密钥保护同一池中的数据集，而无需单一密钥或口令解锁整个池。具体而言，ZFS 加密以下内容：

* 文件和 zvol 数据，
* 文件属性，
* ACL，
* 权限位，
* 目录列表，
* FUID 映射，
* userused/groupused 数据，以及
* 去重数据

一些 ZFS 元数据和信息仍保持未加密状态，包括以下内容：

* 池结构和名称，
* 数据集和快照名称，
* 数据集层级，
* 属性，
* 文件大小，
* 文件空洞，以及
* 去重表

这些未加密的元数据让 ZFS 能执行例行维护操作，如 `zpool scrub`，而无需先解密数据。因此，ZFS 可以在不访问未加密文件内容的情况下检测并修复损坏的加密数据。

要加密现有的未加密数据，将其复制到启用了加密的数据集即可。这样可以将数据迁移到池内的安全位置，前提是提供口令以授权访问。通常，这一过程通过在确认知晓口令等秘密后将加密密钥加载到内存中来完成。密钥加载后，数据集内容即可读写。要重新保护数据集，从内存中卸载密钥。随后 ZFS 会令数据集不可访问，直到再次加载密钥。

启用加密后，一些 ZFS 操作的行为会有所不同。ZFS 在加密前应用压缩，以保留压缩率。ZFS 通常使用 256 位校验和，但加密会用 128 位校验和和加密套件提供的 128 位消息认证码（MAC）替代。这提供了针对恶意数据修改的额外保护。

去重与加密同时使用时，ZFS 仅在加密数据集及其快照和克隆内部执行去重。这防止了加密数据集之间的信息泄露。代价是去重效率降低，因为 ZFS 无法跨整个池比较校验和。即便如此，去重与加密结合仍会暴露哪些块是相同的，并为每个写入的块产生额外的 CPU 开销。

使用 ZFS 加密时还有其他限制。`embedded_data` 特性无法与加密同时使用。启用加密的数据集也不能将 `copies` 属性设置为 3，因为实现将加密元数据存储在通常存放第三份副本的位置。

## 23.6.1. 创建加密数据集

通过在创建数据集时设置 `encryption=on` 属性启用 ZFS 加密。在 `zfs create` 命令之外设置此属性不会追溯启用加密，因为该属性仅在数据集创建时生效。下面的示例在创建加密数据集后挂载时使用口令。

```sh
# zfs create -o encryption=on -o keyformat=passphrase -o keylocation=prompt zroot/secretdata
Enter passphrase:
Re-enter passphrase:
```

如果口令短于 8 个字符，ZFS 会拒绝并创建数据集失败。口令正确输入两次后，ZFS 在池中创建并挂载该数据集。

存储在加密数据集中的任何数据在其存留期间保持加密。从加密数据集复制数据到未加密位置时，复制操作过程中会解密数据。如有必要，可以将新创建的数据集挂载到其他位置。

```sh
# zfs set mountpoint=/secretdata zroot/secretdata
# echo "Hello FreeBSD!" > /secretdata/message
# cat /secretdata/message
Hello FreeBSD!
```

不出所料，数据当前可被任何有权访问该数据集的人读写。

## 23.6.2. 加密数据集的属性

使用以下命令列出数据集的 ZFS 原生加密属性：

```sh
# zfs get encryption,keylocation,keyformat zroot/secretdata
NAME              PROPERTY     VALUE        SOURCE
zroot/secretdata  encryption   aes-256-gcm  -
zroot/secretdata  keylocation  prompt       local
zroot/secretdata  keyformat    passphrase   -
```

`encryption` 属性显示 `aes-256-gcm`，这是目前 ZFS 使用的默认加密方法。如 `SOURCE` 列所示，数据集创建后，此属性和 `keyformat` 属性都无法通过 `zfs set` 修改。有关如何修改其中部分属性的信息，请参阅[更改加密密钥](#id-23.6.5.-geng-gai-jia-mi-mi-yue)。下表列出了 ZFS 使用的加密相关属性：

| 属性             | 说明                                                                                                                                                                                                                              |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| encryptionroot | 当前数据集继承其加密密钥的来源数据集。数据集克隆与其源数据集共享加密密钥。                                                                                                                                                                                           |
| keyformat      | 加密密钥的格式。可能的值为：`raw`、`hex` 和 `passphrase`。`raw` 和 `hex` 密钥都必须为 32 字节长且包含随机值。                                                                                                                                                     |
| encryption     | 用于加密的加密算法，由块加密算法、密钥长度和加密模式组成。创建数据集时必须同时指定 `encryption` 和 `keyformat` 属性。`encryption` 属性的可能值为：`off`（默认）、`on`、`aes-128-ccm`、`aes-192-ccm`、`aes-256-ccm`、`aes-128-gcm`、`aes-192-gcm` 和 `aes-256-gcm`（设置 `on` 时的当前标准）。此属性一经设置便无法更改。 |
| keystatus      | 指示加密密钥是否已成功加载。可能值为 `none`、`available` 和 `unavailable`。                                                                                                                                                                          |
| keylocation    | 指定从何处加载加密密钥。可能值为：`prompt`、`file://path/to/file`、`http://some.address` 和 `https://other.address`。                                                                                                                                |
| pbkdf2iters    | 从口令派生加密密钥时使用的迭代次数。这迫使攻击者执行大量计算成本高昂的哈希操作。随着计算能力增强，此值应从当前默认值 350000 起相应提高。                                                                                                                                                        |

## 23.6.3. 卸载加密密钥

保护加密数据集中存储的数据需要两步：先卸载数据集，再从内存中卸载加密密钥。这一区分很重要。如果加密密钥仍留在内存中，已卸载的数据集并不受保护。务必运行 `zfs unload-key` 来保护数据。要再次访问数据集，请加载密钥并提供口令，如[加载加密密钥](#id-23.6.4.-jia-zai-jia-mi-mi-yue)所述。数据集仍处于挂载状态时，ZFS 会阻止 `zfs unload-key` 运行。

```sh
# zfs unmount zroot/secretdata
# zfs unload-key zroot/secretdata
```

多次运行 `zfs unload-key` 会报错，提示密钥已卸载。

## 23.6.4. 加载加密密钥

加密数据集像其他数据集一样使用前，必须加载其加密密钥并提供口令。此时，`keystatus` 属性仍为 `unavailable`，挂载数据集会因密钥尚未加载而失败：

```sh
# zfs get keystatus zroot/secretdata
NAME              PROPERTY   VALUE        SOURCE
zroot/secretdata  keystatus  unavailable  -
# zfs mount zroot/secretdata
cannot mount 'zroot/secretdata': encryption key not loaded
```

这表明数据集已加密且需要加密密钥。要挂载数据集，运行 `zfs load-key` 并在提示时输入口令：

```sh
# zfs load-key zroot/secretdata
Enter passphrase for 'zroot/secretdata':
# zfs get keystatus zroot/secretdata
NAME              PROPERTY   VALUE      SOURCE
zroot/secretdata  keystatus  available  -
```

接下来，挂载数据集以通过文件系统层级访问它：

```sh
# zfs mount zroot/secretdata
# mount|grep secretdata
zroot/secretdata on /secretdata (zfs, local, noatime, nfsv4acls)
```

要递归加载多个加密密钥，在 `zfs load-key` 后指定 `-r` 选项。如果密钥已加载，ZFS 会以错误消息告知。

```sh
# zfs load-key zroot/secretdata
Key load error: Key already loaded for 'zroot/secretdata'.
```

`keystatus` 属性也会通过报告 `available` 值来确认密钥已加载。要在不加载密钥的情况下验证口令是否正确，使用 `-n` 选项执行干运行。

## 23.6.5. 更改加密密钥

ZFS 还支持更改加密密钥，例如替换口令。此操作无需重新加密数据集。运行以下命令更改加密口令：

```sh
# zfs change-key zroot/secretdata
Enter new passphrase for 'zroot/secretdata':
Re-enter new passphrase for 'zroot/secretdata':
```

运行 `zfs change-key` 要求当前密钥已加载。如果未加载，ZFS 仍会提示输入新密钥，但随后会显示当前密钥未加载的警告。

`zfs change-key` 命令还可以通过 `-o` 选项指定 `keylocation`、`keyformat` 和 `pbkdf2iters` 属性来修改它们。对加密子数据集运行 `zfs change-key` 会使其成为加密根（如果它还不是的话）。要阻止此行为并继续继承父数据集的密钥，指定 `-i` 选项。

请注意，更改父数据集的加密密钥也会更改继承该密钥的子数据集的密钥。因此，这些子数据集继续使用父数据集的加密密钥。如果子数据集应使用不同的密钥，可以在创建时指定不同的 `keyformat`，或对该子数据集运行 `zfs change-key`。这样做会为子数据集创建新的加密根，并中断其从父数据集的加密继承。

加密数据集的 ZFS 克隆始终使用其源数据集的加密密钥。因此，`keystatus`、`keyformat`、`keylocation` 和 `pbkdf2iters` 属性不像其他数据集属性那样继承。相反，它们使用加密根定义的值。要确定加密根，使用只读的 `encryptionroot` 属性。

如果攻击者窃取了加密密钥，使用 `zfs change-key` 更改口令未必能保护现有或新写入的数据。新数据继续使用与现有数据相同的主密钥加密。如果攻击者同时获得用户密钥及其对应的封装主密钥，运行 `zfs change-key` 不会覆盖磁盘上的旧主密钥。因此，旧主密钥可能在一段不确定的时间内仍可用于取证分析。

如果主密钥已泄露，首选方案是安全擦除底层存储设备并创建新池。随后从备份将数据恢复到新池。或者，创建一个新的加密数据集，使用 `zfs send` 和 `zfs receive` 迁移数据，然后运行 `zpool trim --secure` 擦除释放的空间。如果底层硬件不支持安全 TRIM，改用 `zpool initialize`。

## 23.6.6. 发送加密数据集

ZFS 复制基于发送数据集快照，加密数据集也可以创建快照。生成的快照保持加密状态。

```sh
# zfs snapshot zroot/secretdata@snap1
```

除了[通过 SSH 发送加密备份](https://handbook.bsdcn.org/di-23-zhang-z-wen-jian-xi-tong-zfs/pages/VbcZCn1GSRqHbE8CSxpj#id-23.4.10.4.-tong-guo-ssh-fa-song-jia-mi-bei-fen)外，在 `zfs send` 中使用 `-w`（raw）选项可将加密数据块传输到目标池。原始发送具有以下优势：

* 接收系统永远不会看到明文数据。
* 接收系统无需口令，因为不发生解密。
* 无需先加载加密密钥即可发送备份。
* 不可信系统可以接收加密数据，但如果没有加密密钥则无法解密或修改它。

要将上面创建的快照作为原始流发送到池 `backup` 中的数据集 `secret`，输入以下命令：

```sh
# zfs send -w zroot/secretdata@snap1 | zfs recv backup/secret
```

不使用 `-w` 选项时，`zfs send` 以未加密形式传输数据。接收系统随后可以使用不同的密钥重新加密数据。然而，这样做会阻止将来向该目标进行增量原始发送。

## 23.6.7. 引导时加载加密密钥

`zfskeys` [rc(8)](https://man.freebsd.org/cgi/man.cgi?query=rc\&sektion=8\&format=html) 脚本在引导时为 `keylocation` 属性指向 `file://` URL 密钥文件的加密数据集加载加密密钥。该脚本在池导入之后、文件系统挂载之前运行，因此以这种方式处理的数据集会在正常的 ZFS 启动过程中自动挂载。该脚本不处理 `keylocation=prompt` 的数据集；系统引导后需用 `zfs load-key` 手动加载其密钥。

要将之前创建的数据集从口令切换为密钥文件，生成 32 字节的随机数据作为新密钥，限制对该密钥文件的访问，并更改数据集的加密密钥：

```sh
# dd if=/dev/random of=/root/secretdata.key bs=32 count=1
# chmod 600 /root/secretdata.key
# zfs change-key -o keyformat=raw -o keylocation=file:///root/secretdata.key zroot/secretdata
```

在 **/etc/rc.conf** 中启用该脚本：

```sh
# sysrc zfskeys_enable="YES"
```

默认情况下，该脚本加载所有具有 `file://` 密钥位置的加密数据集的密钥。要限制仅加载特定数据集的密钥，在 `zfskeys_datasets` 中列出它们，以空格分隔：

```sh
# sysrc zfskeys_datasets="zroot/secretdata"
```

> **警告：** 任何能读取该密钥文件的人都可以解密数据集。请将密钥文件所有者保持为 `root`、模式为 `600`，并将其存储在本身受保护的存储上，例如加密的根文件系统或仅在引导时连接的可移动介质。
