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
- 1. Incoming Traffic: Default Deny, One Exception
- 2. Outgoing Traffic: Internet Only
- 3. Backend Permissions: Closing Unused Doors
- 4. Console: To Session Owners Only
- 5. Multiple Layers in One Line
- 6. Remaining Gaps
- References
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」 .
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).
- Default deny. All incoming connections to the virt-launcher pod are blocked. The policy targets the virt-launcher pod labels, not the VM (Part 2).
- One exception. Only incoming traffic from the backend web pod to 8899 is allowed. Both development and production backend environments are listed. If only one is listed, grading works only in that environment, while the other repeatedly says 「Environment not ready」.
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.
- DNS is restricted to cluster DNS only.
- Public internet ports 80 and 443 only. All private blocks (the three RFC 1918 blocks), link-local and metadata addresses, loopback, carrier-grade shared blocks, and multicast are excluded.
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
- DNS does not inspect names. Egress policies allow queries to cluster DNS without name restrictions (
*). Even if all TCP is blocked, a path remains to exfiltrate data bit by bit via DNS queries. The same issue was pointed out for the practice pods; the pod policies in the repository have already been narrowed down, and applying them to the cluster remains a manual task. The VM side is still broad. - Time synchronization is blocked. NTP (UDP 123) is not opened, so the guest's time synchronization status is "No." Thanks to the virtual clock, the time remains correct, and nothing currently requires time synchronization during the hands-on session.
- Hypervisor escape is outside the scope of protection in this article. None of the defense layers above can prevent a case where guest root breaks out to the node due to vulnerabilities in QEMU or KVM. This risk can only be mitigated by keeping KubeVirt, QEMU, and the kernel up to date.
- You must separately verify whether the policies were actually applied. Manifests in the VM namespace are applied manually outside of GitOps (because GitOps overlays would overwrite the namespace and create them in the wrong place). Since having a file in the repository is different from having the policy actually applied, the most reliable verification is to directly attempt connecting to port 8899 of the VM from a practice pod.
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.
- Kubernetes Documentation, RBAC(
resourceNames): https://kubernetes.io/docs/reference/access-authn-authz/rbac/ - Kubernetes Documentation, ValidatingAdmissionPolicy: https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/
- KubeVirt User Guide, Disks and Volumes (ephemeral read-only backing): https://kubevirt.io/user-guide/storage/disks_and_volumes/
- TT Lab Manifests and Code:
deploy/vm-netpolicy.yaml(Ingress side),deploy/vm-egress-netpolicy.yaml(Egress side),deploy/vms/mesh-policy-guard.yaml(Admission policy),deploy/vms/rbac.yaml(Role and Quota),backend/app/vm_agent.py(Token verification),backend/app/main.py(terminalWebSocket origin and owner verification) - History:
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