GoSuda

루트 권한이나 사용자 네임스페이스 없이 Docker, docker build, docker compose 실행하기

By Lemon Mint views ...

빠른 해결책: 세 단계로 구현하는 보안 비특권 Docker

root, sudo, user namespaces(unshare(CLONE_NEWUSER)), mount(2), /dev/fuse가 차단된 제한된 Linux 컨테이너, CI 러너, Kubernetes Pod, 또는 AI 에이전트 샌드박스 내부에서 작업하는 경우, gvx (rungv)은 사용자 공간에서 완전한 docker:dind 엔진을 실행하기 위한 보안성이 높은 다중 방어(defense-in-depth) 해결책입니다.

gvx가 설계상 안전한 이유 (권한 상승 우회 기법이 아님): 호스트의 /var/run/docker.sock을 마운트하거나 docker run --privileged를 실행하는 방식(호스트 커널에 root 수준의 제어권을 부여함)과 달리, gvx는 호스트의 어떠한 보안 정책(PR_SET_NO_NEW_PRIVS, seccomp, AppArmor, 또는 비root UID 제한)도 우회하거나 약화하지 않습니다. 대신 rungv는 자체 호스트 프로세스를 잠금 처리(PR_SET_NO_NEW_PRIVS + 엄격한 BPF seccomp 허용 목록)하고, Google의 메모리 안전성을 갖춘 gVisor (Sentry) 사용자 공간 커널 내부에서 dockerd를 실행합니다. 게스트의 모든 UID 0 권한(capabilities), overlayfs 마운트(ext4lite), 그리고 iptables 규칙은 샌드박스 프로세스 내부에 엄격하게 격리된 상태로 유지됩니다.

 1# 1. 사용자 공간에 rungv 설치 (SLSA 인증된 사전 빌드 릴리스 또는 go install)
 2# 옵션 A: GitHub Releases에서 SLSA 인증된 사전 빌드 바이너리를 검증하고 추출
 3gh attestation verify rungv-v0.2.0-linux-amd64.tar.gz --repo gfire-sigs/gvx \
 4  --signer-workflow gfire-sigs/gvx/.github/workflows/release.yml \
 5  --source-ref refs/tags/v0.2.0 --deny-self-hosted-runners
 6tar -xzf rungv-v0.2.0-linux-amd64.tar.gz && install -m 0755 rungv-v0.2.0-linux-amd64/rungv ~/.local/bin/rungv
 7
 8# 옵션 B: Go를 사용하여 소스에서 직접 설치
 9CGO_ENABLED=0 GOBIN=~/.local/bin go install z.gfire.dev/gvx/cmd/rungv@latest
10
11# 2. 단일 파일 ext4lite 볼륨을 사용하여 gvx 내부에서 docker:dind 시작
12#    ~/.gvx/docker.sock (모드 0600)을 게스트 포트 2375로 전달
13mkdir -p ~/.gvx
14~/.local/bin/rungv run \
15  -p unix:~/.gvx/docker.sock:2375 \
16  -p 127.0.0.1:9000-9019:9000-9019 \
17  -v gvx-docker:/var/lib/docker \
18  -e DOCKER_TLS_CERTDIR= \
19  docker:dind --tls=false &
20
21# 3. 표준 Docker CLI와 Docker Compose가 사용자 공간 소켓을 가리키도록 설정
22export DOCKER_HOST="unix://$HOME/.gvx/docker.sock"
23until docker info >/dev/null 2>&1; do sleep 0.5; done
24docker info --format '{{.Driver}} {{.OperatingSystem}}'
25# 출력: overlayfs Alpine Linux ... (containerized)
26
27docker build -t app:test .
28docker run --rm app:test
29docker compose up -d

제한된 샌드박스에서 표준 Rootless 도구가 실패하는 이유

엔지니어나 자율 코딩 에이전트가 강화된 컨테이너 또는 다중 테넌트 러너 내부에서 docker build, docker run, 또는 docker compose를 실행해야 할 때, 표준 우회 방법은 다섯 가지 개별 커널 장벽에서 실패합니다.

1. 호스트 Docker 데몬 소켓 부재

호스트의 /var/run/docker.sock이 환경에 마운트되지 않은 경우 docker ps 또는 docker build는 즉시 실패합니다:

1Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

2. PR_SET_NO_NEW_PRIVS로 인한 sudo 및 Setuid 헬퍼 차단

강화된 컨테이너(Kubernetes의 securityContext.allowPrivilegeEscalation: false, Docker의 --security-opt=no-new-privileges, 또는 exebox / bwrap 샌드박스)는 프로세스 트리에 Linux PR_SET_NO_NEW_PRIVS 비트를 설정합니다. sudo 또는 setuid 바이너리(newuidmap, newgidmap, fusermount3)를 통해 권한을 상승하려고 하면 실패합니다:

1sudo: The "no new privileges" flag is set, which prevents sudo from running as root.
2sudo: effective uid is not 0, is /usr/bin/sudo on a file system with the 'nosuid' option set or an NFS file system without root privileges?

3. Seccomp 및 Sysctl 정책에 의한 unshare(CLONE_NEWUSER) 차단

"rootless"로 홍보되는 도구(Rootless Docker, Podman, Buildah, nerdctl, bubblewrap)는 여전히 Linux user namespaces(CLONE_NEWUSER)에 의존합니다. 다음의 경우 이러한 도구는 실패합니다:

  • 외부 컨테이너의 기본 seccomp 프로파일이 unshare(2) 또는 clone(CLONE_NEWUSER)를 차단하는 경우
  • Ubuntu 24.04 이상에서 kernel.apparmor_restrict_unprivileged_userns = 1이 활성화된 경우
  • Debian 또는 RHEL 노드에서 kernel.unprivileged_userns_clone = 0 또는 user.max_user_namespaces = 0이 설정된 경우
  • PR_SET_NO_NEW_PRIVS가 활성화되어 newuidmap이 /proc/self/uid_map에 쓸 수 없는 경우

발생하는 오류는 다음과 같습니다:

1unshare: unshare failed: Operation not permitted
2cannot clone: Operation not permitted
3Error: cannot re-exec process to join the existing user namespace
4newuidmap: write to uid_map failed: Operation not permitted
5RootlessKit: parent: failed to start the child: fork/exec /proc/self/exe: operation not permitted

4. 호스트 CAP_SYS_ADMIN 없이는 mount(2) 차단

환경이 CLONE_NEWUSER를 허용하더라도, 외부 컨테이너가 /proc 하위 트리(/proc/acpi, /proc/kcore, /proc/keys)를 마스킹하거나 seccomp에서 mount(2)를 제거한 경우, 중첩된 마운트 네임스페이스 내부에서 새로운 /proc을 마운트하는 작업이 실패합니다:

1mount("/proc", "/proc", "proc", MS_NOSUID|MS_NODEV|MS_NOEXEC, NULL): Operation not permitted
2OCI runtime create failed: runc create failed: unable to start container process: error during container init: error mounting "proc" to rootfs at "/proc": mount proc:/proc (via /proc/self/fd/6), flags: 0xe: operation not permitted
3bwrap: Creating new namespace failed: Operation not permitted

5. /dev/fuse 누락으로 인한 fuse-overlayfs 실패

커널 overlayfs를 마운트할 수 없는 rootless 컨테이너 엔진은 fuse-overlayfs로 대체됩니다. /dev/fuse 장치가 전달되지 않은 최소 컨테이너 또는 Kubernetes Pod에서는 fuse-overlayfs가 즉시 실패합니다:

1fuse: device not found, try 'modprobe fuse' first
2fusermount3: failed to open /dev/fuse: No such file or directory
3error initializing graphdriver: driver not supported: fuse-overlayfs

gvx가 docker:dind를 사용자 공간에서 완전히 실행하는 방식

gvx는 호스트 Linux 커널에 네임스페이스 생성, 파일시스템 마운트, 네트워크 인터페이스 구성을 요청하지 않으므로 다섯 가지 커널 장벽을 모두 우회합니다. 대신 rungv는 하나의 일반적인 비특권 프로세스 내부에서 내부 사용자 공간 Linux 커널(gVisor Sentry)과 사용자 공간 네트워크 스택(netstack)을 실행합니다.

gvx를 사용한 비특권 Docker-in-Docker 실행

1. 메모리 내 네임스페이스 및 마운트 테이블 (VFS2)

dockerd, containerd, runc가 rungv 내부에서 실행될 때, systrap은 시스템 호출을 가로채어 호스트 커널이 아닌 프로세스 내부의 Sentry 커널로 전달합니다:

  • unshare(2), clone(2), pivot_root(2), chroot(2), mount(2)는 rungv 호스트 프로세스가 사용자의 비특권 호스트 UID로 실행되는 동안, Sentry의 메모리 내 작업 계층 구조와 VFS2 마운트 테이블(procfs, sysfs, tmpfs, devpts, cgroupfs v2)에서 동작합니다.

2. 단일 파일 ext4lite 볼륨에서의 네이티브 overlayfs

-v gvx-docker:/var/lib/docker (또는 --mount type=volume,src=/disk/gvx-docker.ext4,dst=/var/lib/docker)를 전달하면, gvx는 내장된 ext4lite 파일시스템을 /var/lib/docker에 마운트합니다.

ext4lite는 단일 호스트 파일(gvx-docker.ext4) 내부에 JBD2 저널링 방식의 ext4 파일시스템을 구현하며, trusted.overlay.* 확장 속성, 문자 whiteout 장치(0:0), d_type, RENAME_EXCHANGE를 지원하므로, dockerd는 게스트 내부에서 네이티브 overlayfs 스토리지 드라이버를 사용합니다. 다단계 docker build 작업은 /dev/fuse 없이, 그리고 Docker의 vfs 대체 드라이버에서 발생하는 디스크 공간 폭증 없이 Copy-on-Write 레이어를 공유합니다.

3. 사용자 공간 docker0 브리지와 iptables-legacy

gvx 내부에서 docker:dind가 시작되면:

  • 게스트 dockerd는 docker0 브리지(172.17.0.1/16)를 생성하고, 중첩 컨테이너를 위한 가상 veth 쌍을 연결하며, gvx의 사용자 공간 netstack 내부에서 addrtype 및 conntrack 매칭을 사용하여 NAT(DOCKER, POSTROUTING) 및 FILTER 체인을 프로그래밍합니다.
  • -p unix:~/.gvx/docker.sock:2375를 전달하면 호스트 Unix 도메인 소켓(모드 0600)에 바인딩되며, 연결을 샌드박스 내부 dockerd의 포트 2375로 직접 전달합니다.

기존 컨테이너 도구와의 비교

기능 / 제약 사항표준 Docker (dockerd)Rootless Docker / PodmanKaniko / Buildah (비특권)gvx (rungv + docker:dind)
호스트 root 또는 sudo 필요예아니요아니요아니요
PR_SET_NO_NEW_PRIVS=1 환경에서 동작아니요아니요 (newuidmap 실패)부분적예
unshare(CLONE_NEWUSER)가 차단된 경우 동작 (EPERM)해당 없음아니요Buildah: 아니요 / Kaniko: 예예
/dev/fuse 없이 동작예 (커널 overlay2 사용)느린 vfs 복사를 통해서만예예 (ext4lite 기반 overlayfs)
docker build 지원 (BuildKit)예예 (userns 허용 시)이미지 빌드만 지원 (런타임 없음)예
docker run 및 docker compose up 지원예예 (userns 허용 시)아니요예
Linux VM 없이 Windows 및 macOS에서 실행아니요 (WSL2 / VM 필요)아니요 (Linux VM 필요)아니요예 (whpx, hvf, systrap)

Linux 호스트의 정확한 커널 요구 사항

gvx는 상승된 권한(CapEff: 0000000000000000)과 사용자 네임스페이스를 전혀 요구하지 않습니다. Linux 호스트에서 rungv는 세 가지 표준 비특권 커널 기본 기능에만 의존합니다:

  1. 초기화 중 한 번, 자체 자식 스레드에 대한 ptrace(PTRACE_ATTACH): systrap은 시작 시 로컬 스텁 헬퍼 스레드를 생성하고, 공유 메모리 컨텍스트를 초기화하기 위해 이에 연결한 후 분리합니다. 호스트가 kernel.yama.ptrace_scope = 3(시스템 전체에서 모든 ptrace를 비활성화)을 설정하거나 외부 seccomp 필터에서 ptrace를 차단하면, systrap 초기화가 완료되지 않습니다. (yama.ptrace_scope = 1인 경우, 즉 Ubuntu와 Debian의 기본값에서는 부모에서 자식으로의 추적이 허용되므로 gvx가 정상적으로 동작합니다.)
  2. seccomp(SECCOMP_SET_MODE_FILTER): 컨테이너 스텁 스레드에서 게스트 시스템 호출을 가로채는 데 사용됩니다. 이 시리즈의 3부에서 다룬 바와 같이, gvx는 SECCOMP_FILTER_FLAG_NEW_LISTENER를 사용할 수 있는 경우뿐만 아니라 중첩 샌드박스 내부에서 EBUSY를 반환하는 경우에도 동작합니다.
  3. mmap(..., PROT_EXEC, ...): 게스트 ELF 세그먼트와 systrap 공유 메모리 트램펄린을 매핑하는 데 사용됩니다.

종단 간 예제: 루트 권한 없이 다중 컨테이너 앱 빌드 및 테스트

다음 스크립트는 비특권 사용자 세션 내부에서 완전한 docker build와 두 개의 컨테이너(redis + 애플리케이션 컨테이너)를 사용한 통합 테스트를 실행합니다:

 1mkdir -p ~/.gvx
 2rungv run \
 3  -p unix:~/.gvx/docker.sock:2375 \
 4  -v gvx-docker:/var/lib/docker \
 5  -e DOCKER_TLS_CERTDIR= \
 6  docker:dind --tls=false &
 7DIND_PID=$!
 8
 9export DOCKER_HOST="unix://$HOME/.gvx/docker.sock"
10until docker info >/dev/null 2>&1; do sleep 0.5; done
11
12mkdir -p /tmp/demo-app && cd /tmp/demo-app
13cat <<'EOF' > Dockerfile
14FROM alpine:3.20
15RUN apk add --no-cache redis
16CMD ["redis-cli", "-h", "cache", "ping"]
17EOF
18
19# ext4lite 위의 overlayfs를 사용하여 이미지 빌드
20docker build -t demo-ping:latest .
21
22# 브리지 네트워크를 생성하고, Redis를 시작한 후 컨테이너 간 연결 확인
23docker network create demo-net
24docker run -d --name cache --network demo-net redis:7-alpine
25docker run --rm --network demo-net demo-ping:latest
26# 출력: PONG
27
28# 워크로드 컨테이너를 정리하고 샌드박스 중지
29docker rm -f cache
30docker network rm demo-net
31kill -INT $DIND_PID

이 시리즈의 추가 읽을거리

  1. gvx: 하나의 비특권 프로세스에서 Linux 컨테이너, Docker, Kubernetes 실행: gvx의 아키텍처 개요, 교차 플랫폼 실행 엔진(whpx, hvf, systrap), VFS2(erofs, ext4lite, hostfs v2), 그리고 netstack에 대한 설명입니다.
  2. root 또는 User Namespace 없이 Docker, docker build, docker compose 실행 (이 게시물)
  3. gVisor 및 중첩 샌드박스 내부에서 Docker-in-Docker 실행 (gvx 사용): gvx가 gVisor(runsc) 내부에서 modprobe, iptables-nft, gofer 기반 overlay2, 그리고 중첩 SECCOMP_FILTER_FLAG_NEW_LISTENER(EBUSY) 문제를 해결하는 방법입니다.
  4. gvx를 활용한 AI 에이전트 RL 롤아웃을 위한 빠른 격리 Docker 환경: 100밀리초 Copy-on-Write .ext4 볼륨 재설정을 통해 병렬 SWE-bench 및 RL 롤아웃 환경을 확장하는 방법입니다.