使用 Passkey 解锁加密的 ZFS 卷
摘要
本文介绍了 Revaulter v2,这是一个允许在启动时使用 Passkey(WebAuthn)解锁加密 ZFS 卷的工具。它支持通过移动端 Web 界面进行远程审批,且无需以明文形式存储密钥。
<p><a href="https://lobste.rs/s/m9c8st/unlocking_encrypted_zfs_volumes_with">评论</a></p>
查看缓存全文
缓存时间: 2026/05/11 04:59
# 使用 Passkey 解锁加密 ZFS 卷 | 来自 Blue Ink 来源:https://withblue.ink/2026/05/09/revaulter-encrypted-zfs-passkey.html
如果你运行着使用 ZFS 的服务器,尤其是在家庭实验室中,你可能已经不得不接受磁盘加密方面一个尴尬的权衡。ZFS 原生加密非常棒:它保护静态数据,压缩和去重机制仍然在加密层之上工作,并且你可以加密单个数据集。难点从来不是*启用*加密:难点在于决定将密钥保存在哪里。
管理 ZFS 数据集加密密钥有两种常见方法,但都没有完全令人满意。第一种是要求管理员每次服务器启动时通过 SSH 输入密码短语。这相当安全,因为密钥永远不会存储在机器上,但它使得无人值守重启变得痛苦。如果你的服务器在你旅行时凌晨 3 点崩溃,你的数据将保持离线状态,直到你能找到笔记本电脑和网络。这也促使人们选择他们实际上能记住并能从手机键盘上输入的密码短语,这通常意味着选择更弱的密码。
第二种方法是将密钥放在一个单独的、未加密的分区中的纯文本文件中,并让 ZFS 在启动时读取它。磁盘本身在静止状态下保持加密,因此如果你出售或处置硬件,你是受保护的。然而,任何带走机器或可以读取该分区的人都能免费获得密钥。对于某些威胁模型来说,这没问题,但这在某种意义上并不是真正的*加密*,只是处置卫生而已。
我真正想要的是这样一种设置:加密密钥永远不会以纯文本形式存储在服务器上,我不需要 SSH 登录来解锁任何内容,并且我可以从我的手机上的任何地方批准解锁。这就是我为此构建 **Revaulter** (https://revaulter.italypaleale.me/) 填补的空白,也是我想在此逐步介绍的内容,即我如何用它来在启动时解锁 ZFS 数据集。
## 简要介绍 Revaulter
几年来,我一直在使用 Revaulter 来防止秘密存储在磁盘或环境变量中,而这个月早些时候我发布了 v2 版本,这几乎是一个完全重写的版本。v2 中最大的变化是,Revaulter 现在使用 Passkeys(带有 PRF 扩展的 WebAuthn)直接在浏览器中派生加密密钥。之前的版本依赖于 Azure Key Vault 作为密钥保管员,虽然可行,但操作起来要复杂得多。
总体模型如下:
1. 你服务器上的脚本调用 `revaulter-cli` 请求加密或解密操作。
2. Revaulter 向你发送 webhook 通知(Discord、Slack 或任何 HTTPS 端点)。
3. 你在笔记本电脑或手机上打开 Web UI。
4. 你使用 passkey 进行身份验证并批准请求。
Revaulter Web UI 在移动设备上的截图,显示了一个待处理的 ZFS 密钥解密请求。
对于那些对加密方案更感兴趣的人来说,值得提到的几点;完整细节见 [密码学架构文档](https://revaulter.italypaleale.me/docs/crypto-architecture/)。
- 浏览器使用从你的 passkey 派生的密钥在本地执行实际的加密操作,结果发送回 CLI。端到端加密(E2EE)。Revaulter 服务器本身从未看到明文,也不持有密钥。
- 每个操作的加密密钥是从 32 字节的主密钥确定性派生的,该主密钥在注册时在浏览器中生成,并以包裹形式仅存储在服务器上,包裹密钥从你的 passkey 的 PRF 输出派生。(这允许多个 passkey,例如两个 YubiKey)
- 应用数据使用 AES-256-GCM(或 ChaCha20-Poly1305)加密,CLI 和浏览器之间的传输是混合的:经典 P-256 ECDH 结合 ML-KEM-768,因此即使大多数 passkey 今天还不支持后量子原语,响应信封也是后量子安全的。
- 你可以可选地添加密码作为 passkey 之上的第二因素(特别是如果你担心你的 passkey 不使用后量子加密)。
对于 **ZFS 用例**,我们关心的体验是:
1. 当创建加密数据集时,我们生成一个随机的 32 字节密钥,请求 Revaulter 包裹(加密)它,并将包裹后的信封写入服务器未加密分区上的文件。
2. 在启动时,systemd 单元读取该信封并请求 Revaulter 解包。你会在 Slack/Discord 等收到通知,用你的 passkey 批准请求,然后解包后的密钥直接传入 `zfs load-key`。纯文本密钥从未触及磁盘,也从未在内存中存在超过 ZFS 需要它的时刻。
## 设置 Revaulter 服务器
你需要的是一个可以从你的服务器访问的正在运行的 Revaulter 服务器。[快速入门指南](https://revaulter.italypaleale.me/docs/quickstart/) 详细覆盖了这一点,但核心内容是一个极小的 Docker Compose 文件:
```yaml
# docker-compose.yml
services:
revaulter:
image: ghcr.io/italypaleale/revaulter:2
ports:
- "8080:8080"
volumes:
- ./config.yaml:/etc/revaulter/config.yaml:ro
- ./data:/data
restart: unless-stopped
```
以及一个最小化的 `config.yaml`:
```yaml
databaseDSN: "/data/revaulter.db"
secretKey: ""
baseUrl: "https://revaulter.example.com"
webhookUrl: "https://discord.com/api/webhooks/..."
```
你需要将其置于 TLS 终止的反向代理之后(Traefik、Caddy、nginx 等 - 这是必需的,因为我们使用 WebAuthn 和 WebCrypto),并确保公共 URL 与 `baseUrl` 匹配。`webhookUrl` 是在请求等待批准时被 ping 的地址:私人 Discord 频道非常有效,因为通知很快就能到达你的手机。
服务器启动后,打开你的 Revaulter 实例的 Web UI,注册一个账户,并配对你的 passkey。你会在 UI 中获得一个每用户的 *请求密钥*,这是一个非秘密标识符,CLI 用来向你的账户寻址请求。请妥善保管,因为我们将在此以下的所有命令中使用它。
在其余的指南中,我将假设在 shell 中设置了两个环境变量:
```bash
# 你的 Revaulter 服务器地址
REVAULTER_SERVER="https://revaulter.example.com"
# 你的请求密钥,从 Web UI 复制
REVAULTER_REQUEST_KEY="..."
```
## 在服务器上安装 CLI
CLI 作为一个自包含的 Go 二进制文件发布:你可以从 [发布页面](https://github.com/ItalyPaleAle/revaulter/releases) 获取适合你架构的最新版本。将其放在服务器上的某个地方,例如 `/usr/local/bin/revaulter-cli`。或者,如果你更喜欢用 Docker 运行,还有一个容器镜像 `ghcr.io/italypaleale/revaulter-cli:2`。
我们还需要在本地固定 Revaulter 的锚定密钥,以便我们可以非交互地运行 shell 命令。这是额外的一层保护,以确保 Revaulter CLI 正在与正确的服务器和用户通信。
```bash
# 确保目录存在
mkdir -p /etc/revaulter/cli
chmod 0700 /etc/revaulter/cli
revaulter-cli trust \
--server "$REVAULTER_SERVER" \
--trust-store /etc/revaulter/cli/trust.json \
--request-key "$REVAULTER_REQUEST_KEY"
```
## 包裹数据集密钥
现在我们生成 ZFS 数据集的实际加密密钥,用 Revaulter 包裹它,并将包裹后的 JSON 信封存储在磁盘上。纯文本密钥只在这一行 shell 管道期间存在,并且永远不会以明文形式写入磁盘。
```bash
# 要创建的 ZFS 数据集名称
DATASET_NAME="tank/data"
# 包含数据集的现有 zpool 的名称
ZPOOL_NAME="tank"
# 存储数据集包裹密钥的路径
JSON_KEY_FILE="/etc/revaulter/keys/$DATASET_NAME.json"
# 密钥标签 - 这是任意的,允许你为不同目的保持不同的子密钥
# 这里我们命名为 "zfs-"
REVAULTER_KEY_LABEL="zfs-$(hostname)"
# 额外的认证数据,有助于密钥绑定(可选,但建议)
REVAULTER_AAD="$(hostname):$DATASET_NAME"
# 确保目录存在
mkdir -p "/etc/revaulter/keys/$(dirname "$DATASET_NAME")"
chmod 0700 "/etc/revaulter/keys/$(dirname "$DATASET_NAME")"
# 生成一个随机的 32 字节密钥并用 Revaulter 包裹它
# 密钥编码为 64 个十六进制字符,用于 ZFS keyformat=hex
openssl rand -hex 32 \
| revaulter-cli encrypt \
--server "$REVAULTER_SERVER" \
--request-key "$REVAULTER_REQUEST_KEY" \
--trust-store /etc/revaulter/cli/trust.json \
--algorithm "aes-256-gcm" \
--key-label "$REVAULTER_KEY_LABEL" \
--input - \
--aad "$(echo -n "$REVAULTER_AAD" | base64 -w0)" \
--note "ZFS dataset $DATASET_NAME" \
--format json \
> "$JSON_KEY_FILE"
```
运行命令时,你会通过配置的 webhook 收到通知。打开 Revaulter UI,用你的 passkey 进行身份验证并批准请求。CLI 会阻塞,直到你这样做,然后将 JSON 信封写入 `$JSON_KEY_FILE`。内容看起来像 `{"value": "...", "nonce": "...", "tag": "...", "additionalData": "..."}`,其中值被加密。将此文件存储在未加密的分区上是安全的。
## 解锁脚本
接下来,我们需要一个小脚本,给定包裹后的信封,请求 Revaulter 解包它并在 stdout 上打印纯文本密钥。这是 systemd 将在启动时调用的脚本。
```bash
# 确保目录存在
mkdir -p "/etc/revaulter/unlock/$(dirname "$DATASET_NAME")"
chmod 0700 "/etc/revaulter/unlock/$(dirname "$DATASET_NAME")"
cat < "/etc/revaulter/unlock/$DATASET_NAME.sh" << 'EOT'
#!/bin/bash
set -e
# 等待 Revaulter 服务器可访问
while ! curl -s "$REVAULTER_SERVER/healthz" > /dev/null; do
>&2 echo "Waiting for the Revaulter server"
sleep 3
done
# 休眠一小段随机时间以避免多个单元同时启动时触达速率限制
sleep $[ ( $RANDOM % 3 ) + 1 ]s
# 提交解密请求并将纯文本密钥写入 stdout
cat "/etc/revaulter/keys/$DATASET_NAME.json" \
| revaulter-cli decrypt \
--server "$REVAULTER_SERVER" \
--json - \
--request-key "$REVAULTER_REQUEST_KEY" \
--trust-store /etc/revaulter/cli/trust.json \
--note "ZFS dataset $DATASET_NAME" \
--format raw
EOT
chmod 0500 "/etc/revaulter/unlock/$DATASET_NAME.sh"
```
请求密钥内置在脚本中,这是可以的:它不是一个特别高价值的秘密。它标识*谁*的批准是必需的,但单独它并不授予解密任何东西的能力,这仍然需要你的 passkey 在你的手机上。
Revaulter 发送到 Discord 频道的通知截图
## 创建加密数据集
准备好解锁脚本后,我们最终可以创建 ZFS 数据集。当 `keylocation=prompt` 时,ZFS 从 stdin 读取密钥,所以我们只需将解锁脚本的输出直接管道输入到 `zfs create`:
```bash
zfs create \
-o encryption=aes-256-gcm \
-o keyformat=hex \
-o keylocation=prompt \
"$DATASET_NAME" \
<<< $(/etc/revaulter/unlock/$DATASET_NAME.sh)
```
这将触发 Revaulter 批准请求,流程与包裹步骤相同,因为解包是另一个需要批准的操作。
## 用于在启动时解锁的 systemd 单元
最后一块是一个处理启动时解锁的 systemd 单元。单元需要等待网络在线(因为我们需要与 Revaulter 服务器通信),等待 zpool 导入,然后调用我们的解锁脚本并将其输出馈入 `zfs load-key`。它还需要幂等:如果数据集已经解锁或已经挂载,它应该是一个无操作。
```bash
# 获取数据集的 systemd 安全转义名称
UNIT_NAME=$(systemd-escape "$DATASET_NAME")
# 写入单元文件
cat < "/etc/systemd/system/mount-$UNIT_NAME.service" << 'EOT'
[Unit]
Description=Mount ZFS $DATASET_NAME
Requires=zfs.target network-online.target NetworkManager-wait-online.service
After=zfs.target network-online.target NetworkManager-wait-online.service
StartLimitIntervalSec=0
[Service]
Type=oneshot
RemainAfterExit=true
ExecStart=/bin/sh -c 'while ! zpool list | grep $ZPOOL_NAME; do sleep 1; done; (zfs get keystatus $DATASET_NAME | grep " available" && echo "Already unlocked" || /etc/revaulter/unlock/$DATASET_NAME.sh | zfs load-key $DATASET_NAME) && (zfs get mounted $DATASET_NAME | grep yes && echo "Already mounted: $DATASET_NAME" || zfs mount $DATASET_NAME)'
ExecStop=/bin/sh -c '(zfs get mounted $DATASET_NAME | grep yes && zfs umount $DATASET_NAME || echo "Already unmounted: $DATASET_NAME") && (zfs get keystatus $DATASET_NAME | grep " available" && zfs unload-key $DATASET_NAME || echo "Key already unloaded")'
Restart=on-failure
RestartSec=2s
[Install]
WantedBy=multi-user.target
EOT
chmod 0644 "/etc/systemd/system/mount-$UNIT_NAME.service"
```
然后启用并启动它:
```bash
systemctl daemon-reload
systemctl enable --now "mount-$UNIT_NAME.service"
```
当你启动单元时,你会在手机上收到通知:从 passkey UI 批准,数据集将挂载(如果你没有收到通知,可能是因为新创建的数据集已经加载了密钥)。重启服务器,同样的事情会自动发生:单元等待网络,发出请求,并在你批准之前暂停。如果你从不批准,单元只保持在启动状态,随后的 `systemctl restart` 或另一个批准踢动会使其启动。
由于解锁是一个常规的 systemd 单元,你可以在其他单元中使用 `Requires=mount-$UNIT_NAME.service` 和 `After=mount-$UNIT_NAME.service` 链式启动其他服务。任何依赖于加密卷上数据的内容(例如数据库、数据根目录在该数据集上的 Docker 守护进程等)都会在解锁完成之前等待启动。
## 这给你带来了什么
结果是,在我看来,一个在安全/便利曲线上的真正良好的点。数据集的加密密钥从未以纯文本形式存储在服务器上。启动时不需要 SSH 会话。密钥由 passkey 守护,这是今天普遍可用的最强形式的用户身份验证:抗钓鱼、硬件绑定,攻击者无法猜测或从泄露中重用的东西。
CLI 和浏览器之间的传输是端到端加密的,使用混合后量子原语,因此 Revaulter 服务器本身即使在传输中也不见任何敏感内容。并且因为所有操作都是通过通知和明确批准介导的,你还可以审计每个操作。
当然,有权衡。你需要在某处运行一个可访问的 Revaulter 服务器,这是另一个要操作的东西(虽然它很小且自包含,绝对可以托管在 VPS 或 Raspberry Pi 上)。解锁不是瞬间的:它依赖于我打开笔记本电脑/手机。如果我失去所有注册了我的 passkey 的设备,我会失去解锁数据的能力(提示:确保添加第二个 passkey 并为 [服务器制作备份](https://revaulter.italypaleale.me/docs/backups/))。
对于我的家庭实验室和几个小型生产设置(如我的 Restic 备份),这些权衡是值得的。如果你一直在忍受加密密钥问题的尴尬一半之一,试试它,让我知道进展如何。
相似文章
匿名凭证:图解入门(第二部分)
图解入门系列的第二部分,介绍 Privacy Pass 和 Google 年龄验证提案等真实世界的匿名凭证系统,重点讲解如何防止凭证克隆,并在不牺牲用户隐私的前提下实现富有表现力的证明。
Deepseek v4 Flash 确实惊艳,正准备入手一台 2.5 万美元的电脑
作者称赞 DeepSeek V4 Flash 实现了高性能的本地大语言模型部署,为此计划斥资 2.5 万美元采购硬件,以为对数据隐私要求严格的客户服务。
Podman无根容器与Copy Fail漏洞
本文讨论了Copy Fail漏洞,这是一个影响Podman无根容器的安全漏洞。
@ZhihuFrontier:DeepSeek-V4 RoPE 设计深度分析——来自知乎用户凯源的核心技术洞察。核心痛点…
本文深入剖析了 DeepSeek-V4 中 RoPE(旋转位置编码)设计的技术细节,重点阐述了在 CSA 和 HCA 模块中如何处理 token 压缩与共享 KV 缓存。
Vector by zauth
Zauth 推出 Vector,一款承诺让 Web 应用轻松拥有 AI 安全能力的工具。