为 VM 克隆提供 Firecracker Entropy
Navigation Menu
[中文正文内容]
本文档从高层次的角度探讨了从单个快照恢复多个 VM 克隆的影响。我们首先概述 Linux 随机数生成 (RNG) 设施,然后讨论与克隆状态相关的潜在问题,最后提出一系列建议。值得强调的是,我们的目标是仅针对内核接口防止过时状态成为问题。一些用户空间应用程序或库保留了自己的 entropy 池的等价物,并在克隆后遇到相同的潜在问题。在当前的编程模型下,没有通用的解决方案,我们所能做的就是建议不要在快照前的逻辑中使用它们。
背景
Linux 内核向用户空间公开三个主要的 RNG
接口:/dev/random
和 /dev/urandom
特殊设备,以及 getrandom
系统调用,这些都在 random(7) 手册页 中进行了描述。此外,Firecracker 支持 virtio-rng
设备,该设备可以为 guest VM 提供额外的 entropy。它从 aws-lc-rs
crate 中提取随机字节,该 crate 封装了 AWS-LC
密码库。
传统上,/dev/random
被认为是“真正的”随机性来源,缺点是当 entropy 池耗尽时读取会阻塞。另一方面,/dev/urandom
不会阻塞,这导致人们认为它提供的结果质量较低。
事实证明,在输出质量上的区别实际上很难区分。根据这篇文章,对于 4.8 之前的内核版本,这两个设备都从同一个池中提取输出,唯一的区别是当系统估计 entropy 计数已降低到某个阈值以下时,/dev/random
将阻塞。/dev/urandom
输出被认为对于几乎所有目的都是安全的,但需要注意的是,在系统收集足够的 entropy 进行初始化之前使用它,确实可能会产生低质量的随机数。getrandom
系统调用有助于解决这种情况;默认情况下,它使用 /dev/urandom
源,但会阻塞直到它被正确初始化(可以通过配置标志更改行为)。
较新的内核 (4.8+) 已切换到一种实现,其中 /dev/random
输出来自一个名为阻塞池的池,/dev/urandom
的输出由 CSPRNG
(密码学安全的伪随机数生成器)给出,并且还有一个输入池,用于从系统上可用的各种来源收集 entropy,并用于馈入或播种其他两个组件。 此处提供了非常详细的描述。
此文档中使用这种较新的实现的细节来提出建议。也有一些内核接口用于获取随机数,但它们类似于从用户空间使用 /dev/urandom
(或带有默认源的 getrandom
)。
每当基于快照创建 VM 克隆时,执行都会精确地从先前保存的状态恢复。从 /dev/random
或 /dev/urandom
获取随机字节不会导致从同一快照创建的不同克隆获得相同的结果,因为多个参数(例如计时器数据,或来自 CPU HWRNG
指令的输出,这些指令存在于 Ivy Bridge 或更新的 Intel 处理器上,并在 Firecracker guest 中启用)与每个结果混合在一起。在读取随机值时以及结合 entropy 相关事件(例如中断)时,都会混合额外的位。此外,如果附加了 virtio-rng
,guest 内核最终将从其中接收新的 entropy。这里有两个问题:
- 当存在该功能时,
CPU HWRNG
输出是否始终混合在其中(而不是仅当CPU HWRNG
被信任时)? - 添加的噪声是否足够强,可以认为最终的
RNG
输出与其他所有克隆充分不同?
这两个问题在从快照恢复 VM 后立即特别重要。在 VM 运行“足够”长的时间后,它应该能够自行收集更多 entropy,并且其状态应该与任何其他克隆的状态充分不同。
似乎 CPU HWRNG
总是添加到混合中(如果存在)。更具体地说,第 32 页的第 1 点(在页面顶部) 提到了在 entropy 池输出函数中使用 CPU HWRNG
(如果存在)。第 34 页指出 如果已知 Linux-RNG 具有 CPU
随机数生成器,则来自该硬件 RNG
的数据将在第二步中混合到 entropy 池中。关于 /dev/urandom
后面的随机池和 DRNG
状态的初始化。关于第 35 页上的 DRNG
状态的讨论提到 密钥部分、计数器和 nonce 与 CPU
随机数生成器的输出进行异或(如果存在)。如果不存在,则使用内核函数 random_get_entropy word 获取的高分辨率时间戳与密钥部分进行异或。CPU HWRNG
还用于 DRNG
状态转换函数(如第 36 页第 1 点所述)和重新设定种子操作期间(第 37 页第 2 点)。该文档明确提到何时必须信任 CPU HWRNG
(例如,第 3.3.2.3 节末尾的要点)。
尚不清楚每个克隆恢复后添加的噪声是否足以认为其 RNG
状态对于安全目的而言是不同的。保守的方法是假定陈旧状态对 RNG
输出有重大影响,因此我们应该在每次恢复后基于新数据重新初始化两个源。似乎简单地将数据写入 /dev/urandom
足以混淆 entropy 池,但这些位仅与输入池混合。目前尚不确定此类写入是否对阻塞池产生任何直接影响,并且不太可能导致 CSPRNG
被自动重新设定种子。
与内核 RNG
源进行交互的标准方法记录在 random(4) 手册页 中。它指出,对 /dev/random
或 /dev/urandom
的任何写入都会与输入 entropy 池混合,但不会增加当前的 entropy 估计。还有一个 ioctl
接口,在给定适当的权限的情况下,可以使用该接口将数据添加到输入 entropy 池,同时增加计数,或完全清空所有池。
支持 VMGenID 的 Linux 内核
自 5.18 起,Linux 支持 虚拟机生成标识符 用于 ACPI 系统。自 6.10 起,Linux 还增加了对使用 DeviceTree 而不是 ACPI 的系统的支持。VMGenID
的目的是通知 guest 关于时间偏移事件,例如从快照恢复。该设备在 guest 内存中公开一个 16 字节的密码学随机标识符。Firecracker 实现了 VMGenID
。当从快照恢复 microVM 时,Firecracker 会写入一个新的标识符,并将通知注入到 guest 中。Linux,使用此值 作为其 CSPRNG
的新随机性。引用内核的 random.c 实现:
/*
* Handle a new unique VM ID, which is unique, not secret, so we
* don't credit it, but we do immediately force a reseed after so
* that it's used by the crng posthaste.
*/
因此,在内核处理 VMGenID
通知后,从同一快照启动的所有 VM 中返回的 getrandom()
和 /dev/(u)random
的值是不同的。这在恢复 vCPU 和 Linux CSPRNG
成功重新设定种子之间留下了一个竞争窗口。在 Linux 6.8 中,我们扩展了 VMGenID
,以便在处理通知时向用户空间发出 uevent。用户空间可以轮询此 uevent,以了解何时可以安全地使用 getrandom()
等,从而避免竞争条件。
Firecracker 在 ARM 系统上使用 DeviceTree 绑定支持 VMGenID
,该绑定是在 Linux 6.10 中为该设备添加的。但是,Firecracker 支持的最新 Linux 内核是 6.1。因此,为了在 ARM 系统上使用 VMGenID
,用户需要使用 6.1 内核,并将 DeviceTree 绑定支持从 6.10 反向移植。对于我们的 CI,我们从 6.10 到 6.1 反向移植了相关的更改。想要在 ARM 上使用该功能的 Firecracker 用户需要确保他们在 guest 内核上反向移植这些更改。
请注意,Firecracker 将始终启用 VMGenID
。在没有 VMGenID
驱动程序的内核中,该设备对 guest 无效。
用户空间注意事项
初始化系统(例如 AL2 和其他发行版使用的 systemd
)可能会在启动后保存一个随机种子文件。对于 systemd
,路径是 /var/lib/systemd/random-seed
。为了安全起见,在拍摄快照之前应删除任何此类文件,以防止 guest 将其用于任何目的。还有一个 /proc/sys/kernel/random/boot_id
特殊文件,该文件在启动时使用随机字符串初始化,之后是只读的。从同一快照恢复的所有克隆将隐式地从此文件中读取相同的值。如果这不是期望的结果,则可以通过在 /proc/sys/kernel/random/boot_id
之上绑定挂载另一个文件来更改读取结果。
建议
- 删除
/var/lib/systemd/random-seed
或任何等效文件。 - 如果更改
/proc/sys/kernel/random/boot_id
中存在的值很重要,请在其上绑定挂载另一个文件。 - 如果 microVM 在具有 IvyBridge 或更新的 Intel 处理器(提供
RDRAND
;此外,从 Broadwell 开始提供RDSEED
)的计算机上运行。硬件支持的重新设定种子操作以 Linux 内核定义的节奏进行,对于大多数情况应该足够了。 - 使用
virtio-rng
。如果存在,guest 内核会将该设备用作额外的 entropy 来源。 - 在 5.18 之前的内核上,为了尽可能安全,直接的方法是执行以下操作(在克隆中恢复客户代码之前):
- 打开一个特殊设备文件(
/dev/random
或/dev/urandom
)。请注意,RNDCLEARPOOL
不再对 entropy 池 产生任何影响。 - 发出
RNDADDENTROPY
ioctl 调用(需要CAP_SYS_ADMIN
)以将提供的字节混合到输入 entropy 池中并增加 entropy 计数。这也应该导致/dev/urandom
CSPRNG
被重新设定种子。这些字节可以在 guest 中本地生成,也可以从主机获取。 - 发出
RNDRESEEDCRNG
ioctl 调用(4.14,5.10,(需要CAP_SYS_ADMIN
)),该调用专门导致CSPRNG
从输入池重新设定种子。
- 打开一个特殊设备文件(
- 从 5.18 开始的内核上,当 guest 内核处理
VMGenID
通知时,CSPRNG
将自动重新设定种子。要完全避免竞争条件,用户应遵循与内核 < 5.18 相同的步骤。 - 从 6.8 开始的内核上,用户可以轮询
VMGenID
uevent,该 uevent 是驱动程序在处理VMGenID
通知后重新设定CSPRNG
种子时发送的。
附件 1 包含一个 C 程序的源代码,该程序实现了前三个步骤。 只要 guest 内核版本切换到 4.19(或更高版本),我们就可以依靠 CONFIG_RANDOM_TRUST_CPU
内核选项(或 random.trust_cpu=on 命令行参数)来使用 CPU HWRNG
自动重新填充 entropy 池,因此不再需要第 3 步。绕过第 3 步的另一种方法是附加一个 virtio-rng
设备。但是,我们无法控制 guest 内核何时会从该设备请求随机字节。
附件 1:清除并重新初始化 entropy 池的源代码
#include <errno.h>
#include <fcntl.h>
#include <linux/random.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
void exit_usage() {
printf("Usage: ./rerand [<hexadecimal_string>]\n"
"The length of the string must be a multiple of 8.\n");
exit(EXIT_FAILURE);
}
void exit_perror(const char *msg) {
perror(msg);
exit(EXIT_FAILURE);
}
int main(int argc, char ** argv) {
if (argc > 2) {
exit_usage();
}
size_t len = 0;
struct rand_pool_info *info = NULL;
if (argc == 2) {
len = strlen(argv[1]);
// We want len to be a multiple of 8 such that we have an easier time
// parsing argv[1] into an array of u32s.
if (len % 8) {
exit_usage();
}
info = malloc(sizeof(struct rand_pool_info) + len / 8);
if (info == NULL) {
exit_perror("Could not alloc rand_pool_info struct");
}
// This is measured in bits IIRC.
info->entropy_count = len * 4;
info->buf_size = len / 8;
}
int fd = open("/dev/urandom", O_RDWR);
if (fd < 0) {
exit_perror("Unable to open /dev/urandom");
}
if (ioctl(fd, RNDCLEARPOOL) < 0) {
exit_perror("Error issuing RNDCLEARPOOL operation");
}
if (argc == 1) {
exit(EXIT_SUCCESS);
}
// Add the entropy bytes supplied by the user.
char num_buf[9] = {};
size_t pos = 0;
while (pos < len) {
memcpy(num_buf, &argv[1] + pos, 8);
info->buf[pos / 8] = strtoul(num_buf, NULL, 16);
pos += 8;
}
if (ioctl(fd, RNDADDENTROPY, info) < 0) {
exit_perror("Error issuing RNDADDENTROPY operation");
}
}