샌드박스 안에서 Docker를 실행할 수 있다면 격리가 아닌 이유: Gemini CLI 0.58.0의 네 겹 차단
macOS에서 코딩 에이전트를 sandbox-exec로 감쌌다고 해도 안심하기 이르다. 에이전트가 프로젝트 밖의 파일을 직접 읽지 못하더라도, 호스트에서 실행 중인 Docker Desktop의 daemon socket에 명령을 보낼 수 있다면 이야기가 달라진다. 새 컨테이너를 만들고 호스트 경로를 mount하도록 daemon에 요청하면, 원래 샌드박스의 파일 경계를 우회할 수 있기 때문이다.
Google이 9월 1일 공개한 Gemini CLI 0.58.0에는 macOS Seatbelt profile에서 Docker와 다른 container runtime의 socket, 실행 파일, 서비스 lookup, shared memory를 함께 막는 변경이 들어갔다. 이 패치는 "샌드박스가 파일을 못 읽게 한다"에서 멈추지 않는다. 샌드박스 밖에서 더 강한 권한을 가진 broker에게 요청할 수 있는 경로를 끊는다.
요약: 위험한 것은 docker 명령 하나가 아니다
Gemini CLI의 macOS sandbox는 기본 permissive-open을 포함해 여섯 개의 built-in Seatbelt profile을 제공한다. profile마다 파일 읽기·쓰기와 network 정책은 다르지만, 이번 변경은 여섯 profile 모두에 container runtime 차단 규칙을 넣었다. 동적으로 만드는 base profile과 배포용 embedded profile에도 같은 규칙을 반영했다.
차단 대상은 네 종류다. 첫째는 /var/run/docker.sock과 사용자 홈의 Docker·Colima·OrbStack·Rancher Desktop socket 경로다. 둘째는 Homebrew, /usr/local/bin, 응용 프로그램 bundle 등에 있는 docker, podman, colima, orb 실행 파일이다. 셋째는 com.docker.*와 OrbStack 관련 Mach/XPC service lookup이다. 넷째는 Docker 이름을 쓰는 POSIX shared memory다.
파일 deny 하나로는 부족하다. 서로 다른 운영체제 객체를 사용하는 네 개의 container runtime 접근 경로를 함께 막는다. 출처: github.com/google-gemini/gemini-cli/pull/28935.
한 경로만 막으면 다른 경로가 남을 수 있다. docker binary 실행을 금지해도 프로그램이 socket protocol을 직접 말할 수 있다. socket 파일만 막아도 이미 열린 service나 다른 IPC가 남아 있을 수 있다. 그래서 이번 패치를 "Docker 명령 금지"보다 "권한 broker로 가는 통로를 여러 층에서 닫은 변경"으로 읽는 편이 정확하다.
네 층은 역할도 다르다. 파일 규칙은 잘 알려진 daemon endpoint를 보거나 수정하지 못하게 한다. 실행 규칙은 에이전트가 설치된 client를 손쉽게 호출하는 길을 닫는다. Mach/XPC 규칙은 파일 경로를 거치지 않는 macOS service discovery를 제한한다. shared memory 규칙은 별도 IPC 공간을 통한 접촉을 막는다. 같은 목적의 규칙을 여러 줄 쓴 것이 아니라, 서로 다른 운영체제 객체에 각각 정책을 건 셈이다.
여기서 순서도 중요하다. Seatbelt profile은 먼저 필요한 작업을 허용한 다음 민감한 container 경로를 명시적으로 거부한다. permissive profile이 outbound network를 넓게 허용하더라도 로컬 Docker socket은 일반 인터넷 endpoint와 같은 범주로 남지 않는다. 개발 도구가 network를 써야 한다는 요구와 host daemon 제어 권한을 분리한 것이다.
이 설계는 allow list를 늘릴 때도 기준을 준다. 어떤 빌드가 docker를 필요로 한다고 해서 전체 profile에서 차단을 빼면, 그 session의 모든 shell 명령이 같은 daemon 권한을 얻게 된다. 예외가 필요하다면 작업 전용 environment, 제한된 wrapper, 별도 container sandbox처럼 권한을 사용할 주체와 시간을 좁혀야 한다. 편의를 위해 broker를 다시 열 때는 그것이 파일 한 경로의 예외가 아니라 host 제어면을 여는 결정임을 로그에 남기는 편이 안전하다.
파일 경계가 daemon 경계로 뚫리는 과정
Seatbelt가 작업 디렉터리 밖의 파일 접근을 막는 상황을 생각해 보자. 샌드박스 안의 process는 호스트 파일을 직접 여는 데 실패한다. 하지만 Docker daemon은 샌드박스 밖에서 돌아가며 container 생성과 bind mount를 처리한다. process가 daemon socket에 접근할 수 있으면, 호스트 경로를 새 container에 연결해 달라는 요청을 보낼 수 있다.
이 구조에서는 파일을 실제로 여는 주체가 샌드박스 process가 아니라 daemon과 hypervisor 쪽이다. PR 설명은 Docker Desktop의 VirtioFS mount를 예로 든다. 샌드박스가 적용한 "이 process는 이 경로를 읽지 못한다"는 정책과 daemon의 "요청받은 경로를 container에 mount한다"는 권한이 서로 다른 경계에 있기 때문에 생기는 빈틈이다.
샌드박스 process와 host 파일 사이에 더 강한 권한의 daemon이 있으면 직접 파일 접근 금지만으로 경계를 보장할 수 없다. 출처: github.com/google-gemini/gemini-cli/pull/28935.
이는 Docker만의 특수 사례가 아니다. background service, package manager daemon, browser automation endpoint, cloud metadata proxy처럼 대신 일을 해 주는 broker가 있다면 같은 질문을 해야 한다. 샌드박스 process가 민감한 자원에 직접 닿지 못해도, 더 강한 권한을 가진 서비스에 명령을 전달할 수 있는가?
여섯 profile에 같은 불변식을 넣은 이유
공개 문서에 따르면 macOS profile은 permissive, restrictive, strict와 network를 열거나 proxy로 제한하는 조합으로 나뉜다. 예를 들어 기본 permissive-open은 쓰기를 프로젝트로 제한하지만 넓은 파일 읽기와 outbound network를 허용한다. 사용자는 작업에 맞춰 profile을 바꿀 수 있다.
container runtime 접근 금지는 이런 편의성 선택과 별개여야 한다. network를 허용한다고 Docker daemon까지 허용한다거나, 파일 읽기가 넓다고 host mount 생성 권한까지 따라오면 profile 이름이 약속하는 경계가 흐려진다. PR은 여섯 정적 profile에 같은 deny rule이 있는지 확인하고, embedded copy에도 규칙이 들어 있는지 검사하는 회귀 테스트를 추가했다. 동적 base profile 테스트도 socket, binary, XPC, shared memory 규칙을 확인한다.
PR 작성자가 제시한 자동 테스트는 두 test suite에서 모두 52개 통과를 기대한다. 다만 이 숫자는 해당 PR의 검증 절차다. Gemini CLI가 앞으로도 항상 52개 테스트를 유지한다는 제품 계약은 아니다. 중요한 부분은 profile마다 보안 규칙을 수동 복사해 두고 끝내지 않고, 모든 배포 형태가 같은 불변식을 갖는지 테스트한다는 점이다.
실전 적용: broker 목록을 먼저 그린다
에이전트 sandbox를 설계할 때 허용 파일 목록만 작성하지 말고 권한 broker 목록을 따로 만든다. Docker·Podman socket, SSH agent, credential helper, browser debug port, package manager service, local cloud emulator가 후보가 된다. 각 broker에 대해 endpoint 접근, client binary 실행, service discovery, shared memory나 inherited file descriptor를 나눠 점검한다.
다음으로 "필요하면 넓게 허용"보다 좁은 예외를 사용한다. container build가 필요한 작업이라면 Seatbelt 안에서 호스트 Docker socket을 다시 노출하기보다, Gemini CLI 문서가 설명하는 container 기반 sandbox처럼 목적이 분명한 실행 환경을 고르는 편이 낫다. host socket을 공유해야 하는 중첩 실행은 편리하지만, 그 socket이 사실상 host 권한을 중개한다는 사실을 별도 위험으로 기록해야 한다.
마지막으로 음성 테스트가 아니라 실제 enforcement 테스트를 둔다. dummy socket을 사용자 runtime 경로에 만들고 sandbox 안에서 읽기가 Operation not permitted로 끝나는지 확인한다. 흔한 설치 경로의 runtime binary가 실행되지 않는지, profile을 바꿔도 결과가 같은지 검사한다. 규칙 문자열이 존재하는 테스트와 커널이 실제로 거부하는 end-to-end 테스트는 둘 다 필요하다.
테스트 실패 메시지도 정책의 일부다. 차단된 명령이 단순한 "파일 없음"으로 보이면 사용자는 설치 문제로 오해하고 더 넓은 권한을 줄 수 있다. 어떤 경계가 거부했는지, 필요한 작업을 별도 sandbox로 옮겨야 하는지 알려 주되 자동으로 권한을 확대하지 않는 편이 낫다. 특히 사람이 없는 실행에서는 승인 질문을 허용으로 바꾸지 말고 해당 단계만 중단해야 한다.
운영 기록에는 선택한 profile 이름, 허용한 외부 경로, container runtime 사용 여부를 남긴다. 그래야 같은 명령이 어느 날부터 성공하거나 실패했을 때 모델 변화가 아니라 sandbox 정책과 환경 변화를 먼저 대조할 수 있다. 보안 경계는 설정 파일 한 장이 아니라 실행 시점의 profile, endpoint, 주변 daemon 상태를 합친 결과다.
범위와 주의점
이 글은 Gemini CLI 0.58.0 release, 공개 PR #28935의 설명·diff·테스트, 현재 공식 sandbox 문서를 대조했다. 로컬에 Gemini CLI 0.58.0과 여러 container runtime을 설치해 실제 탈출을 재현하지는 않았다. 따라서 특정 macOS·Docker Desktop 조합에서 exploit이 성공한다고 일반화하지 않고, PR이 밝힌 위협 경로와 적용된 방어를 설명하는 데 범위를 한정한다.
이번 deny list가 모든 미래 runtime이나 사용자 지정 설치 경로를 자동으로 막는 것도 아니다. 새 socket 위치, 다른 binary 이름, 이미 전달된 descriptor처럼 목록 밖의 경로는 별도 검토가 필요하다. 샌드박스는 위험을 줄이는 장치이지 완전한 신뢰 경계라는 보증이 아니다. 운영자는 가장 제한적인 profile을 고르고, 허용된 broker와 mount를 정기적으로 다시 확인해야 한다.


댓글
댓글 쓰기