状态:草稿(2026-10-16),包含我们的清单、代码及部分实测。 策略与权限遵循仓库中的清单,在撰写本文当天,我们以只读方式查询了集群中当前的策略与 Role,并将其与仓库进行了比对(不同之处已在正文中说明)。由于是公开文章,内部地址和服务器名称已做泛化处理。编写规则:以实测数据为准,未能测量的数据标记为「未实测」,在文档中未找到的数据标记为「未确认」。
学生 VM 启动连载 6:隔离
目录
VM 是比实验 Pod 权限更宽的位置。学生在 Guest 内拥有 root 权限,可以加载内核模块、安装任意软件包、随心所欲地使用网络工具。像实验 Pod 那样将 capability 归零的方法在 VM 内是行不通的。因此,VM 的隔离设计重点不在于「禁止在 Guest 内做什么」,而是阻断 「能够从 Guest 触及外部的什么」 。
1. 入站方向:默认阻断,仅留一个例外
Guest 内的评分代理会将收到的字符串 交由 Shell 执行(第 1 篇 ③)。代理监听所有接口(8899 端口)。因此,能够触及该端口的一方可以在该 VM 内以 root 身份运行任何命令。这是必须最先封堵的路径。
起初,看到实验 Pod 无法触及 VM 时我们松了一口气。但其实这得益于实验 Pod 的 出站 策略,而不是 VM 端的防御。只要 Pod 策略被改动哪怕一行,这层保护就会消失。秉持着 防御应当紧邻要保护的对象这一原则,我们在 VM 命名空间中单独应用了入站方向的策略(Cilium 网络策略)。
- 默认阻断。 阻止所有进入 virt-launcher Pod 的连接。策略筛选的目标不是 VM,而是 virt-launcher Pod 的标签(第 2 篇)。
- 一个例外。 仅允许来自后端 Web Pod 到 8899 端口的连接。我们在策略中同时记录了开发和生产两个环境的后端。如果只写一个环境,那么只有该环境能进行评分,而另一个环境会不断报错「环境未准备好」。
应用策略后,我们在双方进行了测试(2026-08-29)。后端 → VM 8899 畅通无阻,而实验 Pod → VM 8899 则因 6 秒超时而被阻断。来自同一命名空间中 其他学生 VM 的连接也会被此默认阻断策略拦截。
第二层:每个 VM 不同的令牌
代理仅接受包含与该 VM 匹配的令牌的请求(采用恒定时间比较)。该令牌对 学生而言并不是秘密。 由于学生是其 VM 内的 root,读取 unit 文件即可看到它。我们要防范的不是学生,而是 当网络策略配置错误时意外触及的其它 Pod。策略是第一层,令牌是第二层。即使知道了某个 VM 的令牌,也无法在其他 VM 上使用。
令牌不存放在 VM 的注解中,而是存放在 cloud-init Secret 中。因为注解会 kubectl get vm -o yaml 在单次操作中被完整带出,而这种输出常被人们互相复制粘贴。
2. 出站方向:仅限互联网
由于 VM 实验需要通过 apt 获取软件包并安装 k3s/kubeadm,起初我们开放了出站方向。但开放的范围过于宽广。我们需要的仅仅是 互联网,而不是 内网。当时 Guest 内的 root 能够触及共享存储、镜像仓库、源码仓库与部署工具、路由器管理界面、Kubernetes API 以及内部 AI 服务。
现在,我们应用了与实验 Pod 相同形式的出站策略。
- DNS 仅限于集群 DNS。
- 公网互联网的 80 和 443 端口 。排除所有私有网段(RFC 1918 的三个网段)、链路本地与元数据地址、环回地址、运营商共享网段及多播地址。
在实施当天(2026-09-05),我们在 Guest 内部进行了直接测试。公网安装脚本地址返回了 200,而内部仓库、镜像仓库、路由器、Kubernetes API 以及内部 AI 服务全部在 3 秒后超时。在同一策略下,10 个 VM 实验的标准答案验证(apt、k3s、Docker、绑定挂载实验)全部通过。
「学生 VM 无法触及集群」这一论断背后最大的支撑就是该策略。Guest 内没有 Kubernetes 服务账户令牌(我们不会将服务账户卷挂载到 VM 中),即便有令牌,通往 API 服务器地址的路径也被阻断了。
会话专属网络:仅限同会话 VM 互通
包含三个节点的 kubeadm 实验需要 VM 之间进行通信。我们为每个会话创建一个策略, 仅允许同会话的 VM 之间 建立隧道(UDP 4789)。实测中,当其他会话的 VM 试图通过发送 10 次 UDP 4789 来闯入该隧道时,对方 Guest 捕获到的数据包数量为 0,并且留下了策略引擎将其丢弃的日志(2026-09-24)。
由于该策略由后端创建和删除,因此后端需要具备创建网络策略的权限。但是, RBAC 无法将 create 权限细化到名称级别(Kubernetes RBAC 文档:在评估权限时,新对象的名称可能尚未确定)。只要赋予权限,后端就可以创建任意形态的策略,甚至可以删除上述默认阻断策略。即便没有恶意攻击者,只要有一行代码因为选择器写错而删除了默认阻断,所有学生 VM 的 8899 端口就会全部暴露,且该事故不会产生任何错误。因此,API 服务器的准入策略(ValidatingAdmissionPolicy)会专门筛选后端的请求并强制执行三点:名称必须符合会话策略的单一格式、禁止修改、格式单一(必须带有相同的会话标签、仅允许单个 UDP 4789 端口)。如果表达式不匹配,则按关闭的方向失败(Fail-close)。
3. 后端的权限:关闭不使用的语句
由于 virt-launcher 的特权需求,VM 命名空间的 Pod 安全标准设置得较为宽松(第 2 篇)。因此,在该命名空间中, 拥有创建 Pod 的权限就等于拿到了通往节点 root 的钥匙。因此,后端服务账户的 Role 设计如下:
| 允许的操作 | 原因 |
|---|---|
| 创建、读取、删除 VM 与 VMI,启动与停止 | 会话的核心业务 |
| VMI 的 console 子资源 | 学生终端(第 1 篇 ⑨·⑩) |
| 创建、读取、删除 Secret 与 Service | cloud-init Secret、三节点会话的邻居名称 |
| 读取 Pod、读取日志 | 状态检查 |
| 创建、删除会话网络策略 | 上述第 2 节,形态由准入策略严格规定 |
| 读取 PVC,仅限 12 个 Golden 名称 | Golden 判定(第 4 篇) |
| 禁止的操作 | 原因 |
|---|---|
| 创建 Pod | 在特权命名空间中通往节点 root 的大门。在 2026-09-24 扩展多 VM 会话权限时,我们顺便关闭了这扇从未使用的门 |
| 创建、删除 PVC,DataVolume | Golden 盘由运维人员创建。如果后端能够删除它,一次回收失误就会抹去所有人的基础环境 |
| 查看 PVC 列表 | 由于 Golden 名称已有规则限定,无需进行遍历 |
将范围缩小到 PVC 名称的原因
最初引入 Golden 盘时,PVC 的读取权限并未限制名称,而是「获取该命名空间下的所有 PVC」。在 2026-10-14 的安全审查中,指出这样会导致能够读取命名空间中的 所有 PVC(包括运维人员 VM 的磁盘)。由于我们需要的仅限 Golden 盘,因此通过 resourceNames 将范围缩小到了 12 个 Golden 名称(6 种 OS × 2 种规格)。
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get"]
resourceNames: [golden-ubuntu-24.04-10gi, golden-ubuntu-24.04-20gi, …] # 12개
resourceNames 仅对已知名称的请求(如 get、update、delete 等)生效,无法对 create 和 deletecollection 进行限制,并且如果限制了 list 和 watch,客户端就必须附加名称字段选择器(RBAC 文档)。因此,「单一命名规则 + 单一 get」的 Golden 设计使得这种收紧成为可能。如果以后增加新的 OS,也需要将其添加到此列表中,但即便遗漏,该 OS 的读取操作也会返回 403 错误,从而退化为通过容器磁盘启动(第 4 篇的替代方案)。如果仓库中的列表与后端的命名规则不一致,测试将会捕获到。
在撰写本文当天检查集群中应用的 Role 时,发现 PVC 规则尚未配置。Golden 尚未启用,而该权限记录在案,计划在启用 Golden 时一并应用。在此期间,即便出现新的 Golden 实验,由于读取会返回 403,系统也会回退以容器磁盘方式启动。这不会导致系统中断,而是按 关闭的方向 降级。
学生能否修改 Golden 盘
不能。基础镜像以 只读方式 挂载到 virt-launcher Pod 中(第 4 篇),而 Guest 中可见的仅是一块合成了基础镜像与可写层的 virtio 磁盘。无论 Guest 内的 root 执行什么 dd 操作,写入都会落入其专属的可写层。基础镜像文件本身位于 Guest 地址空间之外的 Pod 文件系统中。
4. 控制台:仅限会话所有者
在开启学生终端的 WebSocket 时,后端会检查三点:请求是否来自 TT Lab 界面(防止其他网站利用学生的 Cookie 打开 root Shell 的路径)、Cookie 中的用户是否为该会话的所有者、会话是否正在运行。对于其他人的会话和不存在的会话,系统会返回相同的关闭响应以避免被区分。能够访问控制台子资源本身的 Kubernetes 身份仅有后端服务账户。
5. 多层防御一线串联
| 层 | 防御内容 | 实测结果 |
|---|---|---|
| 入站策略 | 除后端外无人能触及 VM | 实验 Pod → 8899 超时 |
| 评分令牌 | 策略失效时误触的 Pod 无法执行命令 | — |
| 出站策略 | VM 无法触及内网与集群 API | 所有内部地址均在 3 秒后超时 |
| 会话网络策略 | 其他会话的 VM 无法闯入本会话网络 | 尝试闯入 10 次,收到 0 个数据包 |
| RBAC + 准入策略 | 后端代码的失误不会演变成节点 root、删除默认策略、删除黄金配置 | — |
| 确认控制台所有者 | 无法打开其他人的会话 shell | — |
6. 剩下的漏洞
- DNS 不区分名称。 出口策略允许不受名称限制地向集群 DNS 发送查询(
*)。即使 TCP 全部被阻断,通过 DNS 查询一点点外泄数据的通道依然存在。在实验 Pod 侧也指出了同样的问题,仓库中的 Pod 策略已经收窄,而应用到集群则需要人工完成。VM 侧目前还比较宽松。 - : :
- : :
- 必须单独确认策略是否实际生效。 VM 命名空间的清单是在 GitOps 之外手动应用的(因为 GitOps 覆盖层会覆盖命名空间,导致其被创建在错误的地方)。文件存在于仓库中与策略实际生效是两回事,最可靠的确认方法是从实验 Pod 实际连接到 VM 的 8899 端口。
TT 的一句话。 在 VM 内部是 root 没关系。重要的是这个 root 在外部能够触及什么。入口只有后端一个,出口只有互联网,后端也只能访问必要的名称!而且“存在于仓库中”与“已应用到集群中”是两码事,所以请直接动手测试。
参考
所有文档均于 2026-10-16 查阅。
- Kubernetes 文档,RBAC(
resourceNames): https://kubernetes.io/docs/reference/access-authn-authz/rbac/ - Kubernetes 文档,ValidatingAdmissionPolicy: https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/
- KubeVirt 用户指南,磁盘与卷(ephemeral 的只读基础): https://kubevirt.io/user-guide/storage/disks_and_volumes/
- TT Lab 清单与代码:
deploy/vm-netpolicy.yaml(入口侧),deploy/vm-egress-netpolicy.yaml(出口侧),deploy/vms/mesh-policy-guard.yaml(准入策略),deploy/vms/rbac.yaml(Role·配额),backend/app/vm_agent.py(Token 验证),backend/app/main.py(terminalWebSocket 的来源与所有者验证) - 记录:
docs/plans/2026-08-29-vm-labs-design.md,docs/plans/2026-09-24-multi-vm-sessions.md,docs/SECURITY-REVIEW-2026-10-14.md