TT Lab
요금제

한국어

시작하기
블로그

블로그

학생 VM 이 뜨기까지 6: 격리 — 게스트 안 root 가 클러스터를 못 건드리는 이유

상태: 초안 (2026-10-16), 우리 매니페스트·코드 + 일부 실측. 정책과 권한은 저장소의 매니페스트를 따랐고, 글을 쓰는 날 클러스터에 걸린 정책·Role 을 읽기 전용으로 조회해 저장소와 같은지 비교했습니다(다른 점은 본문에 적었습니다). 내부 주소와 서버 이름은 공개 글이라 일반화했습니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」, 문서에서 찾지 못한 것은 「미확인」으로 표시합니다.

학생 VM 이 뜨기까지 6: 격리

목차

VM 은 실습 파드보다 넓은 권한을 가진 자리입니다. 학생은 게스트 안 root 이고, 커널 모듈을 올리고, 아무 패키지나 깔고, 네트워크 도구를 마음대로 씁니다. 실습 파드처럼 capability 를 0개로 묶는 방법은 VM 안에서는 통하지 않습니다. 그래서 VM 의 격리는 「게스트 안에서 무엇을 못 하게 하나」 가 아니라 「게스트 밖으로 무엇이 닿을 수 있나」 를 막는 쪽으로 설계했습니다.

학생 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. 들어오는 쪽: 기본 차단, 예외 하나

게스트 안의 채점 에이전트는 받은 문자열을 셸로 실행합니다(1편 ③). 에이전트는 모든 인터페이스(8899 포트)로 듣습니다. 그러니 이 포트에 닿는 쪽은 그 VM 안에서 root 로 아무 명령이나 돌릴 수 있습니다. 가장 먼저 막아야 하는 길입니다.

처음에는 실습 파드가 VM 에 닿지 못하는 것을 보고 안심했습니다. 그런데 그것은 실습 파드의 나가는 쪽 정책 덕분이었지 VM 쪽 방어가 아니었습니다. 파드 정책이 한 줄만 바뀌어도 사라지는 보호입니다. 방어는 지키려는 것 옆에 둔다는 원칙으로, VM 네임스페이스에 들어오는 쪽 정책을 따로 걸었습니다(Cilium 네트워크 정책).

정책을 건 뒤 양쪽에서 쟀습니다(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 서비스였습니다.

지금은 실습 파드와 같은 모양의 나가는 정책을 겁니다.

적용한 날(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. 남은 빈틈

티티 한마디. VM 안에서 root 인 건 괜찮아요. 중요한 건 그 root 가 밖으로 무엇에 닿을 수 있느냐예요. 들어오는 길은 백엔드 하나, 나가는 길은 인터넷만, 백엔드도 꼭 필요한 이름만! 그리고 「저장소에 있다」 와 「클러스터에 걸려 있다」 는 다른 말이니까 직접 두드려 보세요.

참고

문서는 모두 2026-10-16 에 열람했습니다.

로그인하면 좋아요를 누를 수 있습니다

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다