2장. 감옥과 구역 — FreeBSD jail·Solaris Zones
1장의 그 kill은 FreeBSD가 1999년에 막았습니다. 커널 곳곳에 “이 프로세스가 jail 안인가”를 심는 방식이었고, Solaris는 거기에 “얼마나 쓰는가”를 더했어요. 둘 다 잘 돌았는데 오늘 우리가 쓰는 건 이쪽이 아닙니다.
{/* 삽화 자리: 같은 건물 안을 바닥의 테이프 선으로 나눈 여러 구역. 각 구역 안의 사람은 자기 구역이 건물 전체라고 믿는다. 1장 삽화와 달리 이번에는 구역마다 문지기가 서서, 선을 넘어가려는 손짓을 막고 있다. 다만 천장과 배관은 모든 구역 위로 하나로 이어져 있다(공유 커널). 프롬프트: docs/book/illustration-prompts-container.md */}
배경
1장이 남긴 문제는 이것이었습니다. chroot는 경로만 가리고 나머지는 그대로 두는데, 그 나머지를 어떻게 가릴 것인가.
1999년 4월 28일, Poul-Henning Kamp가 FreeBSD에 답을 커밋했습니다. 2000년 FreeBSD 4.0에 실렸고요(📄 문서 기반 — jail(2)).
그가 논문에서 붙든 문제의식은 파일 가리기보다 근본적이었습니다. 유닉스의 root는 권한이 한곳에 몰려 있는 자리입니다. 조금만 떼어 주는 게 어렵고, 한 번 뚫리면 전부 뚫려요. 그래서 root의 일부 능력만 위임할 방법이 필요하다는 게 출발점이었습니다(📄 문서 기반 — Kamp & Watson, 「Jails: Confining the omnipotent root」 §2 ).
그러니까 jail이 풀려던 건 “파일을 어떻게 가릴까”가 아니었습니다. root를 어떻게 잘게 쪼갤까였어요.
스토리
커밋 메시지가 남아 있습니다. 기능 소개서라기보다 만든 사람이 책상에서 쓴 쪽지에 가까워요.
첫 줄은 다들 웅얼거리던 그 “Jail” 기능을 구현했다는 말로 시작합니다. 그리고 자기가 만든 걸 한 문장으로 소개해요.
This is a seriously beefed up chroot kind of thing.
심하게 강화한 chroot 같은 것. 프로세스를 chroot처럼 가두되, superuser가 할 수 있는 일에 훨씬 빡빡한 제약을 걸었다는 설명이 이어집니다.
무엇을 위해 만들었는지도 적어뒀습니다. 감옥 안의 root 비트를 그 감옥에 사는 고객에게 넘겨도 안전할 것 같다고, 사실 그러려고 만든 거라고요. 고객마다 자기 버전의 아파치를 돌리면서 옆집 발등을 밟지 않게 하는 것. 그가 쓴 표현으로는 “real virtual servers”입니다.
메모 몇 줄이 뒤따르는데 여기가 재미있습니다.
I have no scripts for setting up a jail, don’t ask me for them.
jail 설정 스크립트는 없으니 달라고 하지 말라고요. 그리고 각 jail에 /proc을 마운트하라고 권합니다. 그래야 ps가 쓸 만해진다면서요. 이 한 줄은 뒤에서 다시 보겠습니다.
그다음 줄에서 그는 자기 물건의 한계를 스스로 적습니다.
There are no privisions for stopping resource-hogging.
자원을 독차지하는 걸 막을 장치가 없다고요. 오타도 원문 그대로입니다. 만든 사람이 인정한 결함이고, 이 장 후반부가 그 결함을 메우는 이야기입니다.
마지막에는 이런 문장을 남깁니다.
If somebody wants to take it from here and develop it into more of a “virtual machine” they should be most welcome!
누가 여기서부터 가져가서 가상머신에 더 가깝게 발전시켜준다면 환영이라고요. 이 책의 남은 열아홉 장이 그 이후 이야기입니다.
맨 아래에는 후원과 운영 기록이 붙어 있습니다. rndassociates.com이 후원했고, servetheweb.com이 거의 1년간 돌렸다고요. 커밋되기 전에 이미 1년을 프로덕션에서 산 코드였습니다(📄 문서 기반 — FreeBSD 커밋 75c1354).
핵심
jail이 바꾼 것은 질문이었습니다
chroot는 “이 프로세스에게 무엇을 보여줄까”를 물었습니다. jail은 “이 프로세스에게 무엇을 허락할까”를 물었어요.
차이가 커 보이지 않을 수 있는데, 구현이 달라집니다. 보여줄 것을 고르려면 경로 해석 한 군데만 손보면 됩니다. 1장에서 본 그대로예요. 허락할 것을 고르려면 커널 안에서 권한을 확인하는 모든 자리에 “그런데 이 프로세스가 jail 안인가?”를 물어봐야 합니다.
그래서 jail은 시스템 콜 하나가 아니라 커널 전반에 흩어진 검사들의 집합입니다. jail 안의 root는 이런 일을 못 합니다.
| jail이 금지한 것 | chroot는 |
|---|---|
| 커널에 직접 접근하거나 커널 모듈을 올리는 것 | 막지 않음 |
| 파일 시스템을 마운트·언마운트하는 것 | 막지 않음 (우리가 /proc을 붙였죠) |
| 장치 노드를 만드는 것 | 막지 않음 |
| 네트워크 설정·인터페이스·주소·라우팅 테이블을 바꾸는 것 | 막지 않음 |
| raw·divert·routing 소켓에 접근하는 것 | 막지 않음 |
| jail에 할당되지 않은 네트워크 자원에 접근하는 것 | 막지 않음 |
| 대부분의 sysctl 같은 커널 런타임 파라미터를 바꾸는 것 | 막지 않음 |
📄 문서 기반 — Kamp & Watson, 「Jails」 §5 . 원문에는 securelevel 관련 파일 플래그 항목이 하나 더 있는데, BSD 고유 개념이라 뺐습니다.
일곱 줄 전부 chroot는 손대지 않습니다. 1장에서 직접 본 건 마운트와 장치 노드 두 줄이고, 나머지도 같은 이유로 뚫려 있어요. chroot는 권한을 한 톨도 안 건드리니까요.
27년 뒤에도 같은 검사가 있습니다
FreeBSD는 이 환경에 없습니다. 대신 그 목록을 오늘 리눅스 컨테이너에서 그대로 시도해봤어요. 같은 종류의 검사가 지금도 있는지 보려고요.
=== jail(1999) 금지 목록을 오늘 컨테이너에서 ===
커널 모듈 올리기 거부 insmod: Operation not permitted
파일 시스템 마운트 거부 mount: permission denied (are you root?)
장치 노드 만들기 허용
네트워크 주소 바꾸기 거부 ip: RTNETLINK answers: Operation not permitted
라우팅 테이블 바꾸기 거부 ip: RTNETLINK answers: Operation not permitted
커널 파라미터 바꾸기 거부 sysctl: Read-only file system✅ 실측 (Docker Engine 29.7.2 / alpine 3.20 / Apple M1 / macOS 26.6, 2026-09. 전체 소스: docs/book/code/container/ch02/jail-list-today.sh)
여섯 줄 중 다섯 줄이 1999년과 같은 답을 합니다. 마지막 줄만 이유가 다릅니다. 권한 검사에 걸린 게 아니라 /proc/sys가 읽기 전용으로 마운트돼 있어서예요. 같은 결과에 도달하는 길이 하나가 아니라는 뜻이고, 그 이야기는 2부에서 합니다.
그리고 세 번째 줄을 보세요. 1999년 jail이 금지한 걸 오늘 Docker는 허용합니다.
가진 capability : 00000000a80425fb (cap_mknod 포함 — Docker 기본값)
만든 노드로 호스트 디스크 읽기: dd: can't open '/tmp/mysda': Operation not permitted✅ 실측 (같은 환경, 같은 소스)
호스트 디스크를 가리키는 장치 노드를 만드는 것까지는 됩니다. 그런데 그걸 열려고 하면 막혀요.
막는 주체가 바뀐 겁니다. jail은 만드는 것을 금지했고, 오늘은 만들되 쓰는 것을 막습니다. 그 “쓰는 것을 막는 장치”의 이름은 7장에서 나옵니다.
선이 한 겹이 아니라는 게 여기서 처음 보입니다. 하나가 열려 있어도 다음 겹이 막고 있어요.
jail은 PID를 숨기지 않았습니다
1장이 kill로 증명한 그 문제는 어떻게 됐을까요. 논문 §4가 한 줄로 답합니다. 다른 프로세스와 상호작용하는 능력은 같은 jail 안의 프로세스로만 제한된다고요(📄 문서 기반 — Kamp & Watson, 「Jails」 §4 ).
그런데 오해하기 쉬운 지점이 있습니다. jail은 PID 번호를 다시 매기지 않습니다.
jail 안팎의 PID 번호 공간은 하나예요. jail 안 프로세스는 호스트 ps에 진짜 PID 그대로 찍히고, 다만 jail 안이라는 표시가 붙습니다. PHK가 커밋 메모에서 각 jail에 /proc을 마운트하면 ps가 쓸 만해진다고 쓴 것도 같은 맥락이고요. 번호를 숨기는 문제가 아니라 보여줄 자료를 붙이는 문제였습니다.
그러니까 jail이 한 일은 번호를 숨긴 게 아니라 손을 막은 것입니다. PID를 알아도 kill이 거부돼요.
번호를 다시 매기는 일은 리눅스가 9년 뒤에 합니다. 여는 글에서 자바가 자기를 PID 1이라고 믿던 그 일이요.
Solaris는 두 번째 축을 붙였습니다
비슷한 시기 Sun도 같은 문제를 붙들고 있었습니다. 답은 Zones, 우리말로 구역이었고 2004년 2월 Solaris 10 베타에 처음 나와 2005년 정식 릴리스에 실렸습니다.
Zones는 jail이 이름 없이 하던 걸 이름 붙여 나눴습니다. 호스트 전체를 보는 자리가 global zone, 그 안에 사는 게 non-global zone이에요. Sun의 표현으로는 “완전히 격리된 가상 서버”인데, 이 “완전히”가 어디까지인지는 두 절 뒤에 나옵니다.
Sun은 여기에 하나를 더 붙였습니다. 자원 관리요. 2007년 무렵부터 Solaris Containers라는 말은 Zone에 자원 관리 제어를 합친 것을 뜻하게 됩니다(📄 문서 기반 — Solaris Containers ).
지금까지 본 chroot도 jail도 “무엇을 보고 무엇을 할 수 있는가”만 다뤘어요. CPU를 얼마나 쓰는지, 메모리를 얼마나 쓰는지는 아무도 안 봤습니다. 옆 고객이 CPU를 다 써버리면 내 서비스도 같이 느려집니다.
PHK 본인이 그걸 커밋 메시지에 적어뒀죠. 자원 독차지를 막을 장치가 없다고요. Sun은 그 빈칸을 채워 팔았습니다.
격리에 두 축이 있다는 게 여기서 드러납니다. 무엇을 보는가, 그리고 얼마나 쓰는가. Solaris는 둘을 묶었고, 리눅스는 각각 다른 이름으로 만들게 됩니다. 6장과 7장에서 그 둘을 하나씩 손으로 만들어볼 거예요.
공유 커널이라는 전제
Solaris 커널 하나가 리눅스 바이너리를 돌린 적도 있습니다. lx 브랜드라고, zone 안에 Red Hat Enterprise Linux 3 환경을 제공하는 기능이었어요. 커널이 하나라는 게 제약만은 아니었던 셈입니다.
다만 그 하나가 어디까지 제약인지는 따로 봐야 합니다.
jail도 Zones도 가상머신이 아닙니다. 커널은 하나예요. zone은 자기만의 커널이 없기 때문에, 커널 기능을 직접 조작해야 하는 애플리케이션은 zone 안에서 동작하지 않을 수 있습니다(📄 문서 기반 — Solaris Containers ).
방금 본 jail의 금지 목록이 그 이야기입니다. 커널 모듈을 못 올리고, 라우팅 테이블을 못 고치고, sysctl을 못 바꿉니다. 왜냐면 그건 내 커널이 아니라 모두의 커널이거든요.
이 전제는 오늘도 그대로입니다. 여러분이 띄우는 컨테이너도 호스트 커널을 씁니다. 5장에서 그 사실을 프로세스 목록으로 직접 확인할 거예요.
그런데 왜 우리는 이걸 안 쓰나
jail은 1999년부터 프로덕션에서 돌았고 2000년 FreeBSD 4.0에 실렸습니다. Zones는 2005년부터 있었고요. 리눅스 컨테이너가 나오기 한참 전입니다. 그런데 오늘 우리가 쓰는 건 이쪽이 아닙니다.
두 가지로 읽힙니다. 아래는 제 해석입니다.
첫째, 운영체제가 갈렸습니다. jail은 FreeBSD, Zones는 Solaris예요. 특히 Zones는 CDDL 라이선스로 나왔고 Oracle이 2010년 OpenSolaris를 닫으면서 바깥으로 퍼질 길이 좁아졌습니다.
둘째, 그리고 이쪽이 더 중요한데, 1999년의 jail()은 통짜였습니다. 부르면 파일 시스템도 호스트명도 네트워크도 한꺼번에 갈립니다. 프로세스 목록만 나누고 네트워크는 호스트와 공유하고 싶다면? 그때는 방법이 없었어요.
FreeBSD도 결국 이걸 쪼갰습니다. 지금 jail(8)에는 ip4=inherit로 호스트 주소를 제한 없이 쓰게 하는 값이 있고, VNET으로 전용 네트워크 스택을 줄 수도 있고, allow.mount·allow.raw_sockets·allow.set_hostname 같은 파라미터가 항목별로 붙어 있습니다(📄 문서 기반 — jail(8)).
다만 그게 FreeBSD 8.0, 2009년입니다. 리눅스가 프로세스 목록과 네트워크를 따로 나눌 수 있게 된 지 이미 1년 뒤였어요.
리눅스는 처음부터 쪼개서 냈고, FreeBSD는 10년 뒤에 레트로핏했습니다. 그 10년이 갈랐습니다.
정리
- 1999년 4월 28일 PHK가 FreeBSD에
jail()을 커밋했습니다. 본인 표현으로 “심하게 강화한 chroot”. 커밋 전에 이미 1년을 프로덕션에서 돌렸고, 2000년 FreeBSD 4.0에 실렸습니다. - jail은 chroot에 호스트명과 IP 하나를 더하고, 커널 모듈·마운트·장치 노드·네트워크 설정·sysctl을 금지했습니다. 다른 프로세스와의 상호작용은 같은 jail 안으로만 제한됩니다. 1장에서 chroot가 못 막았던 그 kill이에요.
- jail은 PID를 숨기지 않았습니다. 번호는 그대로 두고 손을 막았어요. 번호를 다시 매기는 일은 리눅스가 2008년에 합니다.
- 질문이 바뀐 게 핵심입니다. chroot는 “무엇을 보여줄까”, jail은 “무엇을 허락할까”. 후자는 커널 곳곳에 검사를 심어야 합니다. 실측에서 그 목록 중 다섯 줄이 오늘 컨테이너에서도 같은 답을 했습니다.
- 장치 노드만 예외였는데, 만들 수는 있어도 열 수는 없었습니다. 선이 한 겹이 아니라는 첫 신호입니다.
- Solaris Zones는 2004년 베타·2005년 정식으로 나왔고, global/non-global zone으로 구조를 정리했습니다. 2007년 무렵 Containers는 Zone + 자원 관리를 뜻하게 됩니다. PHK가 없다고 인정했던 그 자원 제어가 여기서 붙습니다.
- 둘 다 커널은 하나를 공유합니다. 오늘 컨테이너도 같은 전제 위에 있습니다.
- 우리 것이 되지 못한 이유는 운영체제가 갈린 것, 그리고 1999년 판이 통짜였던 것입니다. FreeBSD도 2009년에 쪼갰지만 리눅스보다 1년 늦었습니다.
생각해볼 질문
1999년의 jail은 격리를 한 덩어리로 봤습니다. 켜면 전부 갈리고, 끄면 전부 공유됩니다.
현실의 요구는 그렇게 딱 떨어지지 않습니다. 프로세스 목록은 나누고 싶은데 네트워크는 호스트 걸 그대로 쓰고 싶을 때가 있어요. 파일 시스템만 나누고 싶을 때도 있고요.
리눅스는 그래서 다른 길을 갔습니다. 나눌 수 있는 것을 종류별로 쪼개서, 필요한 것만 골라 나누게 만들었어요. 그리고 Solaris가 Containers로 묶어 팔던 두 번째 축, 자원 관리는 또 다른 곳에서 옵니다. 구글이 자기네 데이터센터를 돌리려고 만들어 커널에 밀어 넣었죠.
PHK가 “누가 여기서부터 가져가달라”고 쓴 지 9년 뒤입니다.