Skip to Content

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년 뒤입니다.

▶ 3장. 구글의 컨테이너 — cgroups·namespace·LXC

Last updated on