요약
리눅스 컨테이너를 C 코드 500여 줄로 직접 만들며 격리에 필요한 커널 기능을 하나씩 짚었습니다.
블로거 리지(Lizzie)가 2016년에 쓴 글입니다. 컨테이너를 몇 년 동안 써 왔지만 더 깊이 이해하고 싶어서, 믿을 수 없는 코드를 돌리는 데 필요한 최소한의 제한을 찾으려고 contained라는 작은 런타임을 짰습니다.
왜 중요한가
- Docker가 가려 둔 컨테이너의 실체가 여러 커널 기능의 조합이라는 점을 실제 코드로 보여 줍니다.
- 모든 것을 막는 실서비스 방식과 달리, 어떤 권한이 왜 위험한지 하나씩 따져서 보안 설계의 근거를 드러냅니다.
핵심 내용
- clone()에 네임스페이스(프로세스가 보는 시스템 자원을 따로 나누는 기능) 여섯 종류를 지정해 마운트, PID, 네트워크, 호스트 이름 등을 분리합니다.
- 케이퍼빌리티(루트 권한을 기능별로 쪼갠 단위) 가운데 위험한 것을 버리고, 컨테이너 탈출에 쓰인 CAP_DAC_READ_SEARCH 같은 사례를 설명합니다.
- seccomp(시스템 호출을 걸러 내는 기능)로는 setuid 비트 설정, 중첩 사용자 네임스페이스 생성, ptrace 등 위험한 호출만 골라 막습니다.
- cgroup(프로세스 묶음의 자원 사용을 제한하는 기능)으로 메모리 1GB, 프로세스 64개 같은 상한을 둡니다.
- 사용자 네임스페이스는 심각한 취약점이 여러 번 나왔다며 쓸 수 있을 때만 쓰고, 코드는 고치는 사이 570줄 정도로 늘었다고 밝혔습니다.
HN 반응
- 컨테이너를 보안 경계로 믿으면 안 된다는 지적에, 위협 모델에 따라 다르고 어떤 경계든 공격을 어렵게 할 뿐이라는 반론이 맞섰습니다.
- 더 강한 격리로 Firecracker, gVisor, Kata Containers가 거론됐고, Kata는 실서비스에 붙이기가 매우 어렵다는 경험담도 나왔습니다.
(2016). 댓글이 달린 이전 제출 목록입니다.
https://news.ycombinator.com/item?id=30623372 (250점 | 2022년 3월 10일 | 댓글 27개)
https://news.ycombinator.com/item?id=22232705 (267점 | 2020년 2월 4일 | 댓글 29개)
https://news.ycombinator.com/item?id=15608435 (440점 | 2017년 11월 2일 | 댓글 53개)
2016년에 쓴 글이네요. 지금 다시 쓴다면 무엇이 달라질까요? 예를 들어 cgroups v2나 새로 나온 seccomp 기능이 많은 걸 바꿀까요?
저도 궁금해요. 새로운 세대의 샌드박싱 기술이 뭔가를 바꿀지 말이에요. 예를 들면 이런 거요.
https://github.com/nolabs-ai/nono
공교롭게도 얼마 전에 토이 프로젝트를 하면서 Claude Code에 프롬프트를 잘못 써서, 프로젝트를 "Docker 같은 것"이 아니라 Docker 위에 만들어야 한다고 지정하는 걸 빠뜨렸어요.
그랬더니 제 토큰을 전부 낭비해 가며 특화된 Docker 클론을 만들더라고요. 뭐, 멋지긴 하네요.
저도 예전에 비슷한 맥락으로 https://fzakaria.com/2020/05/31/containers-from-first-principles 라는 글을 썼습니다.
어젯밤에 신뢰할 수 없는 입력으로 Graphviz를 안전하게 돌리는 방법을 찾아봤어요. 요즘 버전의 Graphviz는 신뢰할 수 없는 입력을 온갖 미친 코드 소굴에 넘겨주거든요. Harfbuzz, Pango, libfribidi, libthai, libgraphite2, 거기에 자체 포맷 파서까지 있는데, 하나같이 CVE 전과가 줄줄이라 찰리 맨슨이 좀도둑으로 보일 지경이에요. 게다가 Pango는 멀티스레드라고 하니 비결정성도 각오해야죠. (이 중 대부분은 단순한 ldd 확인으로는 드러나지 않아요. Graphviz가 런타임까지 슬쩍 기다렸다가 graphviz/libgvplugin_pango.so.6을 dlopen하거든요!) 그래서 당연히 샌드박스에 가두고 싶었어요. 악의적인 공격자가 할 수 있는 최악의 짓이 Dickbutt 같은 걸 그리게 만드는 정도가 되도록요. 결국 Claude가 추천한 Bubblewrap으로 500줄도 안 되는 코드가 나왔어요 http://canonical.org/~kragen/sw/dev3/wrapdot :
그래도 코드가 이 정도 되다 보니 뭔가 빠뜨리지 않았다고 장담하진 못하겠어요. 아직 남은 일은 샌드박스 안에서 ImageMagick이나 netpbm을 돌려 PNG 파일을 PPM이나 BMP로 변환하는 건데, 과거에 libpng에 CVE가 있었고 zip 폭탄에도 당연히 취약할 수 있으니까요.
물론 이 방식도 여전히 리눅스 커널 시스템 콜 인터페이스 대부분을 노출해요. 그나마 다행히 /proc과 /dev는 아니지만요. 그리고 노드 세 개짜리 노드-링크 그래프 하나 그리는 데 제가 처음 썼던 리눅스 머신의 전체 가상 메모리보다 많은 양이 필요하다는 건 여전히 미친 소리예요. 그 머신에선 웹 브라우저도 돌리고 커널도 재컴파일했는데 말이죠. 하지만 그건 좀 더 나중에 다룰 이야기고요.
컨테이너를 보안 경계로 여겨서는 안 된다고 생각합니다. 풀 VM조차 탈출당할 수 있고, 실제로 여러 번 탈출당했습니다.
애초에 이런 일이 가능하다는 사실 자체가, 훨씬 나은 접근법이 필요하다는 뜻이라고 봅니다.
제가 아는 한 이 문제의 해법은 Firecracker, gVisor, Kata Containers입니다. VM 기본 요소(x86_64와 ARM64 확장 기능)를 쓰고 코드베이스도 더 가볍습니다.
https://firecracker-microvm.github.io/
https://gvisor.dev/
https://katacontainers.io/
다만 셋 중 어느 것도 직접 써 본 경험은 없습니다. 이것들 위에 뭔가를 만들어 본 분들은 어떻게 생각하시는지 궁금합니다.
수정: Kata는 Firecracker를 쓸 수 있는 것 같으니, 격리 측면에서는 결국 Firecracker냐 gVisor냐입니다. Firecracker는 제가 말한 그 VMM이고, gVisor는 꽤 다릅니다. 시스템 콜을 에뮬레이션하는 사용자 공간 커널에 더 가깝습니다.
저는 smolvm도 추천합니다. Firecracker는 상자를 쓸 만하고 안전하게 만들려면 어느 정도 전문 지식이 필요하거든요.
https://github.com/smol-machines/smolvm
저는 gVisor를 배포해 봤고, Firecracker는 기본적인 테스트를 해 봤고, Kata는 프로덕션 도입을 진지하게 시도해 봤습니다.
Firecracker와 gVisor는 좋은 시스템이고 쓰기에 끔찍하지도 않습니다. 다만 gVisor는 보안 수준이 같지는 않습니다.
Kata는 만들기가 어렵습니다. 프로덕션에서 그걸 구현하는 기술적 노하우는 경외심이 들 정도입니다. 저도 쓰고 싶었지만 클러스터에 통합하기가 너무 복잡해서, 결국 그냥 포기하고 날것의 VM을 클러스터에 미러링했는데 사실 그게 훨씬 쉬웠습니다.
또 Kata는 가상화 마법사가 아닌 이상 컨피덴셜 VM의 가능성을 전부 깨 버립니다.
프로덕션 설계 개요를 보시려면 레드햇의 컨피덴셜 컨테이너 방식을 살펴보세요. 그쪽의 ARO 셀프 호스팅 시스템입니다.
어느 쪽이 더 안전하다는 건가요? 저는 gVisor인 줄 알았는데, 말씀하신 문장은 반대를 암시하는 것처럼 들려서요.
제가 알기로 Kata는 여러 VMM 백엔드를 지원합니다. Firecracker, QEMU, Cloud Hypervisor, 그리고 자체 개발한 Dragonball이 있죠. QEMU를 제외하면 모두 rust-vmm 생태계의 크레이트 위에 만들어졌고, 각자 조금씩 다른 트레이드오프를 택한 것으로 알고 있습니다.
위협 모델에 따라 달라집니다. 보안은 흑백 논리가 아닙니다.
컨테이너가 막아 주는 건 "이 curl 파이프 스크립트가 내 dotfiles를 엉망으로 만들지 않을 거라고 믿을 수 없다" 같은 상황이지, "내가 받은 아무 파일에 샌드박스 탈출 공격이 들어 있을 수 있다" 같은 상황이 아닙니다.
VM으로도 위협 모델에 충분하지 않다면, 대체 뭐가 충분한지 궁금합니다.
그 정의대로라면 우리에겐 "보안 경계"라는 게 아예 없고 "보안 때문에 좀 번거로워지는 것"만 있는 셈이에요. 하지만 "보안 경계"에는 늘 상대적인 강도가 따라붙는 법이고, 일체의 의심 없이 대상을 안전하게 지켜 준다는 보장을 뜻하는 게 아니잖아요.
심층 방어의 원칙은 시간만 충분하면 어떤 시스템이든 뚫릴 수 있지만, 방어 장치를 많이 둘수록 침입을 시도하는 도중에 들키지 않고 시스템을 뚫을 가능성이 낮아진다는 발상에 바탕을 둡니다.
저라면 지금 시점에서는 공유 리눅스 커널을 노출하는 표준 리눅스식 컨테이너를 절대 신뢰하지 않을 겁니다. 올해만 해도 LPE와 컨테이너 탈출 취약점이 너무 많이 나왔습니다. 앞으로 커널이 훨씬 더 강화되면 달라질 수도 있겠지만, 보안 관점에서는 Firecracker 같은 쪽이 더 나은 선택입니다.
Podman은 libkrun을 통해 컨테이너에 KVM 기반 가상화를 쓸 수 있습니다.
podman run --runtime=krun으로요. 이것도 만능 보안 경계는 아니지만 그래도 더 낫다고 봅니다.보안 경계 맞아요. 다만 다른 모든 것과 마찬가지로 완벽하지 않을 뿐이죠.
CHERI 같은 하드웨어 기반 기술이 널리 보급되기 전까지는(근시일이나 중기적으로는 가능성이 극히 낮아 보이지만요) VM 탈출 CVE가 계속해서 나올 거라고 생각합니다.
하드웨어 버그는 지금까지 한 번도 없었으니까요…
치명적인 하드웨어 버그는 치명적인 하이퍼바이저/커널 버그보다 발생 빈도가 한 자릿수 크기(order of magnitude)만큼 낮고, 그래서 항상 뉴스에 나오는 겁니다. 대체로 악용하기도 더 어렵고요. 거의 10년이 지난 지금까지 실전에서 심각한 spectre나 meltdown 악성코드는 본 적이 없습니다.