TT Lab
Blog

한국어 English 日本語 中文

Get started
Blog

Blog

Until Student VMs Are Up 6: Isolation — Why root inside the guest cannot touch the cluster

Status: Draft (2026-10-16), our manifests/code + partial measurements. Policies and permissions followed the repository manifests, and on the day of writing, the cluster's active policies and roles were read-only queried and compared against the repository (differences are noted in the body). Internal addresses and server names were generalized because this is a public post. Writing rules: measured data is primary, unmeasured items are marked as 「Unmeasured」, and items not found in documentation are marked as 「Unconfirmed」.

Until Student VMs Are Up 6: Isolation

Table of Contents

A VM is a place with broader permissions than a practice pod. The student is root inside the guest, loads kernel modules, installs any package, and uses network tools freely. Methods like locking capabilities down to 0, as done for practice pods, do not work inside a VM. Therefore, VM isolation was designed not around 「what cannot be done inside the guest」 but around blocking 「what can be reached outside the 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
Figure: Where Student VMs Can and Cannot Reach

1. Incoming Traffic: Default Deny, One Exception

The grading agent inside the guest runs received strings with shell(Part 1 ③). The agent listens on all interfaces (port 8899). Therefore, anything reaching this port can run arbitrary commands as root inside that VM. This is the first path that must be blocked.

At first, we were relieved to see that practice pods could not reach VMs. However, that was thanks to the practice pods' egress policy, not VM-side defense. It is a protection that disappears if the pod policy changes by even a single line. Following the principle that defense is placed next to what you want to protect, we applied an ingress policy specifically to the VM namespace (Cilium Network Policy).

After applying the policy, measurements were taken from both sides (2026-08-29). Backend → VM 8899 passed, while practice pod → VM 8899 was blocked by a 6-second timeout. Connections coming from other student VMs in the same namespace also trigger this default deny.

Second Layer: Token per VM

The agent accepts requests only if they match a per-VM token (constant-time comparison). This token is not a secret to the student. Since the student is root on their VM, they can see it by reading unit files. What we aim to block is not the student, but other pods that might reach it due to a misconfigured network policy. The policy is the first layer, and the token is the second layer. Knowing one VM's token cannot be used on another VM.

The token is placed in the cloud-init Secret, not VM annotations. Annotations are kubectl get vm -o yaml copied as-is all at once, and that output is often pasted around among people.

2. Outgoing Traffic: Internet Only

VM practice requires receiving packages via apt and installing k3s and kubeadm, so outgoing traffic was initially opened up. However, the open scope was too wide. What is needed is the internet, not the home network. At that time, places reachable by root inside the guest included shared storage, image registries, source repositories and deployment tools, router management screens, Kubernetes APIs, and internal AI services.

Now, we apply an egress policy shaped like the practice pods.

On the day of application (2026-09-05), measurements were taken directly from inside the guest. Public installation script addresses received 200, while internal repositories, registries, routers, Kubernetes APIs, and internal AI services all timed out after 3 seconds. Under the same policy, answer sheet validation for 10 VM practices (apt, k3s, docker, bind mount practices) all passed successfully.

This policy is the biggest reason why 「student VMs cannot touch the cluster」. The guest does not have a Kubernetes service account token (service account volumes are not mounted to VMs), and even if it did, paths to the API server address are blocked.

Session-Only Network: Within the Same Session VMs Only

A 3-node kubeadm practice requires VMs to communicate with each other. A policy is created per session to open tunnels (UDP 4789) only between VMs in the same session . In measurements where a VM from a different session tried to intrude on that tunnel by sending UDP 4789 10 times, 0 packets were captured on the peer guest, and logs showed the policy engine dropped them (2026-09-24).

Because the backend creates and deletes this policy, the backend needs permission to create network policies. However, RBAC cannot narrow down create by name(Kubernetes RBAC documentation: the name of a new object may not be determined at the time permission is evaluated). With just permissions, the backend can create policies of any shape and could even delete the default deny policy mentioned above. Even without an attacker, a single line of code with a wrong selector deleting the default deny would open port 8899 for all student VMs, and that incident would produce zero errors. Therefore, the API server's admission policy (ValidatingAdmissionPolicy) intercepts backend requests to enforce three things: the name must match the session policy pattern only, modification is rejected, and the shape is fixed (same session label, single UDP 4789). If the formula is incorrect, it fails closed.

3. Backend Permissions: Closing Unused Doors

The VM namespace sets the Pod Security Standards to the most lenient because of virt-launcher privileges (Part 2). Therefore, in this namespace, the permission to create pods is literally a door to node root. The backend service account's Role is structured accordingly.

What can be done Why
Create, read, delete VMs/VMIs, start and stop Core business of the session
VMI console subresource Student terminal (Part 1 ⑨·⑩)
Create, read, delete Secrets and Services cloud-init Secret, neighbor names in 3-node sessions
Read pods, read logs Status check
Create and delete session network policies Section 2 above; shape is locked down by admission policy
PVC read, 12 golden names only Golden grading (Part 4)
What cannot be done Why
Create pods A door to node root in a privileged namespace. When adding multi-VM session permissions on 2026-09-24, this unused door was closed together.
Create and delete PVCs, DataVolumes Goldens are created by operators. If the backend could delete them, a single reclaim mistake would erase everyone's base image.
List PVCs Golden names are fixed by rule, so there is no need to browse them.

Why Narrows Down by PVC Name

When golden disks were first introduced, PVC read permissions were specified without names as 「get PVCs in this namespace」. During the security audit on 2026-10-14, it was pointed out that this allowed reading all PVCs in the namespace (including operator VM disks). Since only goldens are needed, we narrowed it down to resourceNames leaving only twelve golden names (6 OSes × 2 sizes).

- apiGroups: [""]
  resources: ["persistentvolumeclaims"]
  verbs: ["get"]
  resourceNames: [golden-ubuntu-24.04-10gi, golden-ubuntu-24.04-20gi, …]   # 12개

resourceNames works for requests that know the name (get, update, delete, etc.), cannot narrow down create and deletecollection, and narrowing down list and watch requires clients to attach name field selectors (RBAC documentation). Thus, golden's design of 「one naming rule + one get」 made this narrowing possible. Adding an OS requires adding it to this list as well, but even if missed, reading that OS returns 403 and it simply opens as a container disk (alternative in Part 4). If the repository list and the backend naming rule mismatch, the test catches it.

Reading the Role applied to the cluster on the day of writing, the PVC rule was not yet present. Goldens have not been turned on yet, and it is documented that this permission will be applied together when goldens are enabled. In the meantime, even if a golden practice is created, reading returns 403, so it opens as a container disk. Instead of failing to block, it falls back fail-closed .

Can Students Modify the Golden Disk?

No. The base image is mounted read-only to the virt-launcher pod (Part 4), and what is visible to the guest is a single virtio disk combining the base and the write layer. No matter how much root inside the guest does dd , writes go to its own write layer. The base file itself resides outside the guest's address space, in the pod's filesystem.

4. Console: To Session Owners Only

When opening the student terminal WebSocket, the backend checks three things: whether the request came from the TT Lab UI (blocking other sites from opening a root shell using a student's cookie), whether the cookie user is the owner of that session, and whether the session is running. Other sessions and non-existent sessions are closed with the same code to avoid distinguishing them. The only Kubernetes identity that can access the console subresource itself is the backend service account.

5. Multiple Layers in One Line

Layer Blocked Threat Measurement
Ingress Policy Nothing except the backend can reach the VM Practice pod → 8899 timed out
Grading Token Pods reaching the VM due to policy misconfiguration cannot run commands —
Egress Policy VM cannot reach internal network or cluster APIs All internal addresses timed out after 3 seconds
Session Network Policy VMs from other sessions cannot intrude into my session network 10 intrusion attempts, 0 packets
RBAC + Admission Policy Mistakes in backend code do not escalate to node root, deletion of default policies, or deletion of golden images. —
Verify console ownership Cannot open another user's session shell —

6. Remaining Gaps

A word from TT. It's fine to be root inside the VM. What matters is what that root can reach outside. Ingress through the backend only, egress to the internet only, and for the backend, only the necessary names! Also, "existing in the repository" and "being applied to the cluster" are two different things, so go test it out directly.

References

All documents were accessed on 2026-10-16.

Sign in to like

Comments

No comments yet.

Sign in to leave a comment