TT Lab
博客

한국어 English 日本語 中文

开始
博客

博客

学生 VM 启动连载 6:隔离——为什么 Guest 内的 root 无法触及集群

状态:草稿(2026-10-16),包含我们的清单、代码及部分实测。 策略与权限遵循仓库中的清单,在撰写本文当天,我们以只读方式查询了集群中当前的策略与 Role,并将其与仓库进行了比对(不同之处已在正文中说明)。由于是公开文章,内部地址和服务器名称已做泛化处理。编写规则:以实测数据为准,未能测量的数据标记为「未实测」,在文档中未找到的数据标记为「未确认」。

学生 VM 启动连载 6:隔离

目录

VM 是比实验 Pod 权限更宽的位置。学生在 Guest 内拥有 root 权限,可以加载内核模块、安装任意软件包、随心所欲地使用网络工具。像实验 Pod 那样将 capability 归零的方法在 VM 内是行不通的。因此,VM 的隔离设计重点不在于「禁止在 Guest 内做什么」,而是阻断 「能够从 Guest 触及外部的什么」 。

학생 VM 이 닿는 곳과 못 닿는 곳 들어오는 쪽 — 기본 차단, 예외 하나 ✓ 백엔드 웹 파드 → 8899 채점 에이전트 · VM별 토큰 ✗ 실습 파드 → 8899 시간 초과로 막힘(실측) ✗ 다른 학생 VM → 아무 포트 같은 네임스페이스여도 학생 VM (게스트 안 root) 서비스 계정 토큰 없음 · 디스크는 쓰기 층 하나만 virtio 로 보인다 나가는 쪽 — 인터넷만 ✓ DNS · 공인 인터넷 80/443 apt · 설치 스크립트 ✗ 사설 대역 전체 저장소 · 레지스트리 · 공유기 ✗ 쿠버네티스 API 시간 초과(실측) 백엔드 서비스 계정의 권한 (저장소의 Role) ✓ VM·VMI 만들기·지우기, console 서브리소스 ✓ Secret·서비스 만들기·지우기(세션 것) ✓ PVC get — 골든 이름 12개만(resourceNames) ✗ 파드 create, PVC create·delete, DataVolume 이 네임스페이스는 특권 파드를 허용한다 — 파드 create 는 곧 노드 root
图:学生 VM 可以触及与无法触及的地方

1. 入站方向:默认阻断,仅留一个例外

Guest 内的评分代理会将收到的字符串 交由 Shell 执行(第 1 篇 ③)。代理监听所有接口(8899 端口)。因此,能够触及该端口的一方可以在该 VM 内以 root 身份运行任何命令。这是必须最先封堵的路径。

起初,看到实验 Pod 无法触及 VM 时我们松了一口气。但其实这得益于实验 Pod 的 出站 策略,而不是 VM 端的防御。只要 Pod 策略被改动哪怕一行,这层保护就会消失。秉持着 防御应当紧邻要保护的对象这一原则,我们在 VM 命名空间中单独应用了入站方向的策略(Cilium 网络策略)。

应用策略后,我们在双方进行了测试(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 相同形式的出站策略。

在实施当天(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. 剩下的漏洞

TT 的一句话。 在 VM 内部是 root 没关系。重要的是这个 root 在外部能够触及什么。入口只有后端一个,出口只有互联网,后端也只能访问必要的名称!而且“存在于仓库中”与“已应用到集群中”是两码事,所以请直接动手测试。

参考

所有文档均于 2026-10-16 查阅。

登录后即可点赞

评论

还没有评论。

登录后即可发表评论