TECH 으로 돌아가기
TECH HACKER NEWS 오늘 9분 읽기 28 READS

리눅스 컨테이너를 500줄 코드로: 커널 격리 메커니즘 뜯어보기

컨테이너는 이제 배포와 운영의 기본 단위가 됐지만, 정작 그 안에서 어떤 커널 기능들이 맞물려 격리를 만들어내는지 설명할 수 있는 실무자는 많지 않다. Lizzie Dixon이 공개한 "Linux containers in 500 lines of code"는 바로 그 공백을 겨냥한 글이다. 도커나 런타임 라이브러리에 의존하지 않고, 약 500여 줄(개정 후 70줄가량 늘었다)의 C 코드로 신뢰할 수 없는 코드를 돌릴 최소한의 제약 집합을 직접 구현해 본 기록이다. 저자가 분명히 선을 긋듯, 이 접근은 실제 노출된 환경에서 따라야 할 모범이 아니다. 운영에서는 막을 수 있는 모든 것을 막아야 한다. 다만 '무엇이 범주적으로 위험한가'를 이해하려면, 반대로 최소한만 남기고 깎아내 보는 실험이 효과적이다.

여러 메커니즘이 겹쳐 만드는 격리

리눅스 컨테이너는 단일 기능이 아니라 서로 중첩되고 보완하는 여러 커널 메커니즘의 조합이다. 네임스페이스, capabilities, seccomp, setrlimit, cgroups가 대표적인데, seccomp·capabilities·setrlimit은 시스템 콜로 다루고 cgroups는 파일시스템을 통해 접근한다. 문제는 각 메커니즘의 책임 범위가 명확하지 않고 상당 부분 겹친다는 점이다. 그래서 '무엇을, 어디서 제한하는 것이 최선인가'를 판단하기가 까다롭다. 저자는 이 코드에서 마운트, PID, IPC, 네트워크 장치, 호스트명/도메인명을 네임스페이스로 분리한다. 핵심은 clone 시스템 콜이다. fork()류의 바탕이 되는 이 콜에 플래그를 넘겨, 부모와 다른 속성(다른 루트를 마운트하거나 자체 호스트명을 설정하는 등)을 가진 자식 프로세스를 만든다. 이때 자식과 부모는 socketpair로 통신하고, 부모는 자식의 사용자 네임스페이스를 설정한 뒤 프로세스 트리가 끝날 때까지 대기한다.

사용자 네임스페이스의 양날

사용자 네임스페이스(user namespace)는 비교적 최근 기능으로, 흩어진 격리 동작을 통합할 잠재력을 지녔다. 하지만 커널에 이 기능을 켜서 컴파일하는 순간 capabilities의 의미가 시스템 전역에서 바뀌어 혼란을 키울 수 있다. 더 중요한 건 권한 상승 취약점의 온상이 되어 왔다는 사실이다. 저자가 인용한 "Understanding and Hardening Linux Containers"는, 보안상 큰 이점에도 불구하고 민감한 성격과 서로 충돌하는 보안 모델, 방대한 신규 코드 탓에 심각한 취약점이 계속 발견되어 왔다고 지적한다. 특히 이 문제들은 컨테이너를 쓰지 않는 시스템에서도, 단지 커널 버전이 사용자 네임스페이스를 지원할 만큼 최신이라는 이유만으로 나타나곤 한다. 글이 쓰일 당시 리눅스는 이를 기본적으로 꺼두었지만, 많은 배포판이 제한적으로 켜는 패치를 적용했다. 저자는 중첩된 사용자 네임스페이스를 막을 것이므로 결국 호스트에 이 기능이 컴파일됐는지가 관건이며, 가용할 때만 쓰는 쪽을 택한다.

capabilities, 어디까지 깎아낼 것인가

글의 분량 대부분은 capabilities에 할애된다. capabilities는 리눅스에서 '루트임'이라는 권한을 잘게 쪼갠 것으로, 예컨대 네트워크 장치를 다루는 CAP_NET_ADMIN과 모든 파일을 읽는 CAP_DAC_OVERRIDE를 분리해 관리하게 해준다. 저자는 man 7 capabilities의 알고리즘을 따라 inheritable과 ambient 집합을 비우고 permitted·effective를 필요한 것만 남긴다. 자기 자신의 집합만 비우면 자식 파일의 capabilities로 권한을 되찾게 되는데(bash가 ping을 호출할 수 있는 원리다) 이를 막기 위해서다.

버려야 할 대표적 권한의 이유도 구체적이다. CAP_DAC_READ_SEARCH는 open_by_handle_at에 임의의 file_handle을 넘겨, 사실상 inode 번호를 무차별 대입해 임의 파일을 읽게 한다. 2014년 Sebastian Krahmer가 도커 내부에서 호스트 파일을 읽어낸 shocker.c가 이 권한을 썼다. 사용자 네임스페이스가 없을 때 CAP_FSETID는 setuid 비트를 지우지 않고 setuid 실행 파일을 수정하게 해, 위험한 setuid 루트 바이너리를 디스크에 남길 수 있다. CAP_MKNOD는 실제 하드디스크 장치 파일을 다시 만들어 리마운트 후 읽고 쓰게 할 수 있고, CAP_SYS_RAWIO는 /proc/kcore·/dev/mem 등으로 호스트 메모리와 IO 포트에 접근하게 한다. CAP_SYS_BOOT(reboot·kexec), CAP_SYS_MODULE(모듈 로딩), CAP_SYS_TIME(네임스페이스화되지 않은 시스템 시각 변경), CAP_SYS_NICE(PID 네임스페이스를 모르는 스케줄러를 악용한 서비스 거부)도 같은 맥락에서 제거 대상이다.

남겨도 되는 권한의 논리

흥미로운 부분은 '버리지 않는' 권한을 추적한 대목이다. 여러 곳에서 CAP_DAC_OVERRIDE가 CAP_DAC_READ_SEARCH와 같은 기능(open_by_handle_at)을 노출한다고 하지만, 저자가 shocker.c와 커널 코드를 확인한 결과 그렇지 않았다. CAP_DAC_OVERRIDE 단독으로는 마운트 네임스페이스 밖을 읽지 못한다는 것이다. CAP_FOWNER·CAP_LEASE·CAP_LINUX_IMMUTABLE은 마운트 네임스페이스 안의 파일에만 작용하고, CAP_IPC_OWNER는 IPC 네임스페이스를 존중하는 함수에서만 쓰이므로 별도 IPC 네임스페이스에 있는 한 허용해도 된다. CAP_NET_ADMIN·CAP_NET_BIND_SERVICE·CAP_NET_RAW 역시 가상 브리지로 네트워크를 격리하고 네트워크 네임스페이스 안에 두면 문제되지 않는다. 요컨대 어떤 권한의 위험성은 그 자체가 아니라 '네임스페이스화 여부'와 '다른 격리와의 조합'으로 결정된다는 판단이 일관되게 깔려 있다.

실무적으로 이 글이 주는 교훈은 분명하다. capabilities를 떨어뜨리는 것만으로는 충분치 않다. procfs 일부에 대한 쓰기처럼 capabilities를 모두 버린 뒤에도 루트에게 허용되는 동작이 남아 있어, seccomp나 cgroups 같은 추가 제약이 필요한 이유가 여기 있다. 또한 설정 순서가 중요하다. 특정 capabilities 없이는 마운트를 바꿀 수 없고, 시스템 콜을 제한한 뒤에는 unshare를 할 수 없다. 블랙리스트 방식으로 권한과 시스템 콜을 막을 때는 새로 추가된 항목이 없는지 반드시 확인해야 한다는 당부(실제로 이 코드에도 그로 인한 버그가 있었다)는, 거부 목록 기반 보안의 근본적 한계를 그대로 보여준다. 결국 이 500여 줄은 완성된 런타임이 아니라, 컨테이너가 '왜 안전하거나 안전하지 않은가'를 코드 수준에서 사고하도록 돕는 참조 교재에 가깝다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://blog.lizzie.io/linux-containers-in-500-loc.html
SHARE
NEXT · CHOOSE

변화를 읽었다면,
내가 만들 수익 구조를 고릅니다.

정보를 더 모으는 데서 멈추지 않고, 광고·외주·판매·중개·구독 중 내 상황에 맞는 출발점을 정해보세요.

21가지 수익 구조 살펴보기 →
처리 중...