跳到主要内容

安全

模型​

作业代码被视为恶意代码。第一道屏障是 VM;第二道是对运行 VM 的进程的约束。

libkrun 的文档说得很明确:guest 和 VMM 处于同一个安全上下文中。 逃出 VM 就会落入 rc vmm 进程内部,拥有该进程的权限。因此隔离由 VM 加上对这个进程的约束共同构成——而目前这种约束因平台而异。

作业的各层防护(两个平台通用)​

层实现方式
每个作业一台 VM每个作业在一台新 VM 中运行,结束后销毁;作业之间不传递任何东西
只读 rootroot 是以只读方式挂载的 ext4(/dev/vda);只能写入 /tmp 的 tmpfs(256 MiB)
临时状态tmpfs 随 VM 一起消失
无网络krun_disable_implicit_vsock + krun_add_vsock(ctx, 0):没有 TSI,没有网络接口
无 virtiofsroot 是磁盘,而不是共享 host 目录;virtiofs 无法阻止 guest 访问 host 的其他目录
按语言设上限vCPU 和 RAM 在目录中限定;超时默认 10 s,最长 300 s,超时向进程组发送 SIGKILL
输入限制stdin ≤ 1.000.000 字节,最多 32 个文件,代码 ≤ 4 MiB,文件名不能含 /、不能含 ..、不能以 . 开头
父进程退出,VM 随之退出host 崩溃时不会留下孤儿 VM

按平台​

项目macOSLinux
hypervisorHypervisor.frameworkKVM
创建 VM 的权限二进制上的 entitlement com.apple.security.hypervisor对 /dev/kvm 的访问权限
VM 随父进程退出fd 3 上的看守管道Pdeathsig
超时向进程组发送 SIGKILL向进程组发送 SIGKILL
VM 进程使用非特权 uid/gid—spec 支持(krun_setuid/krun_setgid)
LandlockmacOS 上不存在作用于每台作业 VM 的 vmm 进程
seccompmacOS 上不存在denylist 作用于所有线程(TSYNC)

在 macOS 上,对 vmm 进程的约束只有 entitlement 和进程组。

Linux 上的进程约束​

在把进程交给 libkrun 之前,作业 VM(root 在磁盘上,无网络)的 vmm 会应用:

机制放行拒绝
Landlock(文件)读取 root 磁盘和已加载的库、/proc、/sys;写入控制台和 VM 自己的目录(run/<id>/);/dev/kvm 及其 ioctl文件系统的其余所有部分
Landlock(网络,ABI ≥ 4)无所有 TCP bind 和 connect(ABI 10 起包括 UDP)
Landlock(作用域,ABI ≥ 6)—发往本进程以外的信号和抽象 socket
Landlock(Unix socket,ABI ≥ 9)VM 自己的 socket对其他 VM socket 的 connect
seccomp其余execve、ptrace、mount、unshare/setns、内核模块、kexec、bpf、perf_event_open、密钥环、时钟、swap、reboot、userfaultfd 等(EPERM)

libkrunfw 在 Landlock 之前加载,因为 libkrun 只在 krun_start_enter 内部才打开它,约束生效之后这个 open 会失败。seccomp 有意采用 denylist 而不是 allowlist:allowlist 需要在带 KVM 的 Linux 上逐一测量 libkrun 的每个 syscall,漏掉一个就会让所有 VM 起不来。

已实测:在 Ubuntu 26.04(内核 7.0,aarch64,Landlock ABI 8)上,对受约束的进程本身测试:读取策略外的文件、 在磁盘旁写入、TCP、execve 和 personality 均被拒绝;读取磁盘和创建自己的 socket 被允许。去掉约束后,同一个探针 全部成功(失败证明)。

未实测: 在带 KVM 的 Linux 上,完整的 microVM 在约束下运行(测试 VM 没有 /dev/kvm)。

已知缺口: Landlock ABI < 9 时,内核不控制按路径对 Unix socket 的 connect;一台 VM 的进程仍能访问另一台 VM 的 socket。rc doctor 会给出警告。RUNNER_SANDBOX=off 可关闭约束用于诊断,并在 stderr 中提示。

持久化 machine​

machine 放弃了作业隔离的一部分(它保存状态,并且可以有网络),每一处放开都是显式声明的,并有底线:

层实现方式
网络默认关闭不加 --net(或 net,单独 -p)时,machine 没有网卡;只有 -p 而没有 --net 时,host 能访问端口,但 guest 出不去
出站策略在 vmm 进程自身中运行的用户态网关,没有 TSI:按域名 allow-host、CIDR、--block-private;名单外的域名返回 NXDOMAIN;IP 只有来自被允许的域名时才有效。见网络与卷
任何规则都打不开的底线云 metadata 及所有 link-local;host 的 loopback 只能通过 --allow-cidr 127.0.0.1 放开,永远不能通过 DNS 学到的 IP 放开(DNS rebinding)
拒绝记录每一次被拒绝的出站都记入 machine 目录下的 egress-denials.log
不可读取的凭据guest 只看到占位符;网关只在 binding 主机的 TLS 中把它替换为真实值,使用 machine 自己的 CA(name constraints)。URL 中出现占位符会被以 403 拒绝。见凭据
secrets--secret-env/--secret-file 在 start 时读取,仅在内存中发给 guest;不写入注册表,也不写入 machine 磁盘
SSH agentguest 通过转发的 socket 使用 host 的密钥;私钥不会进入 VM
:ro 卷保护在设备层面,而不只是挂载选项:guest 的 root 执行 mount -o remount,rw 也打不开该卷(已测试)
嵌套虚拟化在 host 不支持嵌套的地方拒绝 --nested;通过 API 时,只有 rc serve --allow-nested-virt 才允许
屏幕查看器只监听 host 的 127.0.0.1,无密码

machine 不防护的内容:guest 的 root 能读取自身进程的环境变量,所以交给 machine 的 secret 对在其中运行的代码是 可见的。凭据替换也无法阻止服务器在响应体中把该值返回。

端到端测试证明了什么​

属性证明
无网络在 VM 内连接 1.1.1.1 失败;故意打开网络时,测试不通过
只读 root写入 /etc 失败
作业之间无状态一个作业写入 /tmp 的文件在下一个作业中不存在
内存上限在 512 MiB 的 VM 中分配 2 GiB 失败,不影响 host
超时死循环的作业被杀掉,没有 VM 残留

API​

HTTP API 没有认证。默认只监听 127.0.0.1。没有带认证的代理挡在前面时,不要暴露到 loopback 之外。

镜像构建​

镜像的 pull 在 host 上进行。其余构建步骤在一台单独的、联网的 VM 中运行,因为安装 e2fsprogs 和 GNU tar 以及 warmup(例如 apk add)都需要网络。构建 VM 的网络到不了作业 VM。构建 VM 使用的 Alpine 基础 tarball 在使用前会校验 sha256。各语言的镜像来自官方 OCI 镜像。

待办​

  • 在带 KVM 的 Linux 上验证完整的 microVM 在 Landlock + seccomp 下运行;
  • API 认证(目前交给代理);
  • 在 Landlock ABI < 9 的内核上实现 VM 之间的 Unix socket 隔离。