상태: 초안 (2026-10-16), 우리 매니페스트·코드 + 일부 실측. 정책과 권한은 저장소의 매니페스트를 따랐고, 글을 쓰는 날 클러스터에 걸린 정책·Role 을 읽기 전용으로 조회해 저장소와 같은지 비교했습니다(다른 점은 본문에 적었습니다). 내부 주소와 서버 이름은 공개 글이라 일반화했습니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」, 문서에서 찾지 못한 것은 「미확인」으로 표시합니다.
학생 VM 이 뜨기까지 6: 격리
목차
VM 은 실습 파드보다 넓은 권한을 가진 자리입니다. 학생은 게스트 안 root 이고, 커널 모듈을 올리고, 아무 패키지나 깔고, 네트워크 도구를 마음대로 씁니다. 실습 파드처럼 capability 를 0개로 묶는 방법은 VM 안에서는 통하지 않습니다. 그래서 VM 의 격리는 「게스트 안에서 무엇을 못 하게 하나」 가 아니라 「게스트 밖으로 무엇이 닿을 수 있나」 를 막는 쪽으로 설계했습니다.
1. 들어오는 쪽: 기본 차단, 예외 하나
게스트 안의 채점 에이전트는 받은 문자열을 셸로 실행합니다(1편 ③). 에이전트는 모든 인터페이스(8899 포트)로 듣습니다. 그러니 이 포트에 닿는 쪽은 그 VM 안에서 root 로 아무 명령이나 돌릴 수 있습니다. 가장 먼저 막아야 하는 길입니다.
처음에는 실습 파드가 VM 에 닿지 못하는 것을 보고 안심했습니다. 그런데 그것은 실습 파드의 나가는 쪽 정책 덕분이었지 VM 쪽 방어가 아니었습니다. 파드 정책이 한 줄만 바뀌어도 사라지는 보호입니다. 방어는 지키려는 것 옆에 둔다는 원칙으로, VM 네임스페이스에 들어오는 쪽 정책을 따로 걸었습니다(Cilium 네트워크 정책).
- 기본 차단. virt-launcher 파드로 들어오는 연결은 전부 막습니다. 정책이 고르는 대상은 VM 이 아니라 virt-launcher 파드의 라벨입니다(2편).
- 예외 하나. 백엔드 웹 파드에서 8899 로 오는 것만 허용합니다. 개발과 운영 두 환경의 백엔드를 모두 적었습니다. 한쪽만 적으면 그 환경에서만 채점이 되고, 다른 쪽은 「환경이 준비되지 않았습니다」 만 되풀이합니다.
정책을 건 뒤 양쪽에서 쟀습니다(2026-08-29). 백엔드 → VM 8899 는 통과했고, 실습 파드 → VM 8899 는 6초 시간 초과로 막혔습니다. 같은 네임스페이스의 다른 학생 VM 에서 오는 연결도 이 기본 차단에 걸립니다.
두 번째 겹: VM 마다 다른 토큰
에이전트는 VM 마다 다른 토큰이 맞는 요청만 받습니다(상수 시간 비교). 이 토큰은 학생에게 비밀이 아닙니다. 학생은 자기 VM 의 root 라서 유닛 파일을 읽으면 보입니다. 막으려는 것은 학생이 아니라 네트워크 정책이 잘못됐을 때 닿아 버린 다른 파드입니다. 정책이 첫 겹, 토큰이 두 번째 겹입니다. 한 VM 의 토큰을 알아도 다른 VM 에는 쓸 수 없습니다.
토큰은 VM 의 어노테이션이 아니라 cloud-init Secret 에 둡니다. 어노테이션은 kubectl get vm -o yaml 한 번에 그대로 딸려 나오고, 그 출력은 사람들이 서로 붙여 넣기 때문입니다.
2. 나가는 쪽: 인터넷만
VM 실습은 apt 로 패키지를 받고 k3s·kubeadm 을 설치해야 하므로 나가는 쪽을 처음에는 열어 두었습니다. 그런데 열린 범위가 너무 넓었습니다. 필요한 것은 인터넷이지 집 안 네트워크가 아닙니다. 그때 게스트 안의 root 가 닿을 수 있던 곳은 공유 스토리지, 이미지 레지스트리, 소스 저장소와 배포 도구, 공유기 관리 화면, 쿠버네티스 API, 내부 AI 서비스였습니다.
지금은 실습 파드와 같은 모양의 나가는 정책을 겁니다.
- DNS 는 클러스터 DNS 로만.
- 공인 인터넷의 80·443 만. 사설 대역 전체(RFC 1918 의 세 대역), 링크 로컬과 메타데이터 주소, 루프백, 통신사 공유 대역, 멀티캐스트는 예외로 뺍니다.
적용한 날(2026-09-05) 게스트 안에서 직접 쟀습니다. 공인 설치 스크립트 주소는 200 을 받았고, 내부 저장소·레지스트리·공유기·쿠버네티스 API·내부 AI 서비스는 전부 3초 시간 초과였습니다. 같은 정책 아래에서 VM 실습 10개의 정답지 검증(apt·k3s·도커·바인드 마운트 실습)이 모두 통과했습니다.
「학생 VM 은 클러스터를 못 건드린다」 의 가장 큰 이유가 이 정책입니다. 게스트에는 쿠버네티스 서비스 계정 토큰이 없고(VM 에 서비스 계정 볼륨을 붙이지 않습니다), 토큰이 있더라도 API 서버 주소로 가는 길이 막혀 있습니다.
세션 전용 망: 같은 세션 VM 끼리만
노드 셋짜리 kubeadm 실습은 VM 끼리 통신해야 합니다. 세션마다 정책 하나를 만들어 같은 세션의 VM 끼리만 터널(UDP 4789)을 엽니다. 실측에서 다른 세션의 VM 이 그 터널에 끼어들려고 UDP 4789 를 10번 보내자 상대 게스트에서 잡힌 패킷이 0개였고, 정책 엔진이 버렸다는 기록이 남았습니다(2026-09-24).
이 정책을 백엔드가 만들고 지우므로 백엔드에 네트워크 정책 생성 권한이 필요합니다. 그런데 RBAC 는 create 를 이름으로 좁힐 수 없습니다(쿠버네티스 RBAC 문서: 새 객체의 이름은 권한을 판단하는 시점에 정해져 있지 않을 수 있다). 권한만 주면 백엔드는 아무 모양의 정책이나 만들 수 있고, 위의 기본 차단 정책을 지울 수도 있습니다. 공격자가 아니어도 선택자를 잘못 쓴 코드 한 줄이 기본 차단을 지우면 모든 학생 VM 의 8899 가 열리고, 그 사고는 아무 오류도 내지 않습니다. 그래서 API 서버의 입장 정책(ValidatingAdmissionPolicy)이 백엔드의 요청만 골라 세 가지를 강제합니다. 이름은 세션 정책의 꼴 하나뿐, 고치기는 거부, 모양도 하나(같은 세션 라벨, UDP 4789 하나)입니다. 식이 틀리면 닫힌 쪽으로 실패합니다.
3. 백엔드의 권한: 쓰지 않는 문은 닫는다
VM 네임스페이스는 virt-launcher 의 특권 때문에 파드 보안 기준을 가장 느슨하게 둡니다(2편). 그러니 이 네임스페이스에서는 파드를 만들 수 있는 권한이 곧 노드 root 로 가는 문입니다. 백엔드 서비스 계정의 Role 은 그래서 이렇게 생겼습니다.
| 할 수 있는 것 | 왜 |
|---|---|
| VM·VMI 만들기·읽기·지우기, 시작·정지 | 세션의 본업 |
| VMI 의 console 서브리소스 | 학생 터미널(1편 ⑨·⑩) |
| Secret·서비스 만들기·읽기·지우기 | cloud-init Secret, 노드 셋짜리 세션의 이웃 이름 |
| 파드 읽기, 로그 읽기 | 상태 확인 |
| 세션망 정책 만들기·지우기 | 위 2절, 모양은 입장 정책이 못박는다 |
| PVC 읽기, 골든 이름 12개만 | 골든 판정(4편) |
| 할 수 없는 것 | 왜 |
|---|---|
| 파드 만들기 | 특권 네임스페이스에서 노드 root 로 가는 문. 2026-09-24 에 다중 VM 세션 권한을 더하면서, 쓰지 않던 이 문을 함께 닫았다 |
| PVC 만들기·지우기, DataVolume | 골든은 운영자가 만든다. 백엔드가 지울 수 있으면 회수 실수 하나로 모두의 바탕이 사라진다 |
| PVC 목록 보기 | 골든 이름이 규칙으로 정해져 있어 훑을 일이 없다 |
PVC 이름으로 좁힌 이유
골든 디스크를 처음 넣을 때 PVC 읽기 권한은 이름 없이 「이 네임스페이스의 PVC get」 이었습니다. 2026-10-14 보안 점검에서, 그러면 네임스페이스의 모든 PVC(운영자 VM 의 디스크 포함)를 읽을 수 있다는 점이 지적됐습니다. 필요한 것은 골든뿐이므로 resourceNames 로 골든 이름 열둘(OS 여섯 × 크기 둘)만 남겼습니다.
- 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 하나」 라는 골든의 설계가 이 좁히기를 가능하게 했습니다. OS 를 더하면 이 목록에도 더해야 하는데, 빠뜨려도 그 OS 는 읽기가 403 이 되어 컨테이너 디스크로 열릴 뿐 실습은 뜹니다(4편의 대체). 저장소의 목록과 백엔드의 이름 규칙이 어긋나면 시험이 잡습니다.
글을 쓰는 날 클러스터에 걸린 Role 을 읽어 보니 PVC 규칙이 아직 없었습니다. 골든은 아직 켜지 않았고, 이 권한은 골든을 켤 때 함께 적용하기로 기록돼 있습니다. 그동안 골든 실습이 생겨도 읽기가 403 이라 컨테이너 디스크로 열립니다. 막히지 않고 닫힌 쪽으로 떨어지는 순서입니다.
골든을 학생이 고칠 수 있나
없습니다. 바탕은 virt-launcher 파드에 읽기 전용으로 마운트되고(4편), 게스트에 보이는 것은 바탕과 쓰기 층을 합친 virtio 디스크 하나뿐입니다. 게스트 안의 root 가 아무리 dd 를 해도 쓰기는 자기 쓰기 층으로 갑니다. 바탕 파일 자체는 게스트의 주소 공간 밖, 파드의 파일시스템에 있습니다.
4. 콘솔: 세션 주인에게만
학생 터미널의 웹소켓을 열 때 백엔드는 세 가지를 봅니다. 요청이 TT Lab 의 화면에서 왔는지(다른 사이트가 학생의 쿠키로 root 셸을 여는 길을 막는다), 쿠키의 사용자가 그 세션의 주인인지, 세션이 실행 중인지입니다. 남의 세션과 없는 세션은 같은 코드로 닫아 구별해 주지 않습니다. 콘솔 서브리소스 자체에 닿을 수 있는 쿠버네티스 신원은 백엔드 서비스 계정뿐입니다.
5. 여러 겹을 한 줄로
| 겹 | 막는 것 | 실측 |
|---|---|---|
| 들어오는 정책 | 백엔드 말고는 VM 에 닿지 못한다 | 실습 파드 → 8899 시간 초과 |
| 채점 토큰 | 정책이 틀렸을 때 닿은 파드가 명령을 못 돌린다 | — |
| 나가는 정책 | VM 이 내부망·클러스터 API 에 닿지 못한다 | 내부 주소 전부 3초 시간 초과 |
| 세션망 정책 | 다른 세션의 VM 이 내 세션 망에 끼어들지 못한다 | 끼어들기 시도 10번, 0 패킷 |
| RBAC + 입장 정책 | 백엔드 코드의 실수가 노드 root·기본 정책 삭제·골든 삭제로 번지지 않는다 | — |
| 콘솔 주인 확인 | 남의 세션 셸을 열지 못한다 | — |
6. 남은 빈틈
- DNS 는 이름을 가리지 않습니다. 나가는 정책은 클러스터 DNS 로 가는 질의를 이름 제한 없이(
*) 허용합니다. TCP 가 다 막혀도 DNS 질의로 데이터를 조금씩 내보내는 길은 남습니다. 같은 문제가 실습 파드 쪽에서도 지적돼, 저장소의 파드 정책은 이미 좁혔고 클러스터 적용은 사람이 할 일로 남아 있습니다. VM 쪽은 아직 넓습니다. - 시각 동기가 막혀 있습니다. NTP(UDP 123)를 열지 않아 게스트의 시각 동기 상태는 「아니오」 입니다. 가상 시계 덕에 시각은 맞았고 지금 실습 중에 시각 동기가 필요한 것은 없습니다.
- 하이퍼바이저 탈출은 이 글의 방어 밖입니다. 게스트 root 가 QEMU 나 KVM 의 취약점으로 노드에 나오는 경우는 위의 어떤 겹도 막지 못합니다. 이 위험은 KubeVirt·QEMU·커널을 제때 올리는 것으로만 줄입니다.
- 정책이 실제로 걸렸는지는 따로 확인해야 합니다. VM 네임스페이스의 매니페스트는 GitOps 밖에서 손으로 적용합니다(GitOps 오버레이가 네임스페이스를 덮어써서 엉뚱한 곳에 만들어지기 때문입니다). 파일이 저장소에 있는 것과 정책이 실제로 걸린 것은 다르므로, 가장 확실한 확인은 실습 파드에서 VM 의 8899 로 실제로 붙어 보는 것입니다.
티티 한마디. VM 안에서 root 인 건 괜찮아요. 중요한 건 그 root 가 밖으로 무엇에 닿을 수 있느냐예요. 들어오는 길은 백엔드 하나, 나가는 길은 인터넷만, 백엔드도 꼭 필요한 이름만! 그리고 「저장소에 있다」 와 「클러스터에 걸려 있다」 는 다른 말이니까 직접 두드려 보세요.
참고
문서는 모두 2026-10-16 에 열람했습니다.
- 쿠버네티스 문서, RBAC(
resourceNames): https://kubernetes.io/docs/reference/access-authn-authz/rbac/ - 쿠버네티스 문서, 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(토큰 확인),backend/app/main.py(terminal웹소켓의 출처·주인 확인) - 기록:
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