GoSuda

Uruchamianie Dockera, docker build i docker compose bez uprawnień roota i bez przestrzeni nazw użytkowników

By Lemon Mint views ...

Szybkie rozwiązanie: bezpieczny nieuprzywilejowany Docker w trzech krokach

Jeśli pracujesz w zablokowanym kontenerze Linux, na runnerze CI, w Podzie Kubernetes lub w piaskownicy agenta AI, gdzie root, sudo, przestrzenie nazw użytkownika (unshare(CLONE_NEWUSER)), mount(2) oraz /dev/fuse są zablokowane, gvx (rungv) stanowi bezpieczne, wielowarstwowe rozwiązanie umożliwiające uruchomienie pełnego silnika docker:dind w przestrzeni użytkownika.

Dlaczego gvx jest bezpieczny z założenia (nie jest obejściem eskalacji uprawnień): W przeciwieństwie do montowania hostowego /var/run/docker.sock lub uruchamiania docker run --privileged (które dają kontrolę równoważną rootowi nad jądrem hosta), gvx nie omija ani nie osłabia żadnej polityki bezpieczeństwa hosta (PR_SET_NO_NEW_PRIVS, seccomp, AppArmor ani ograniczeń UID innych niż root). Zamiast tego rungv zabezpiecza własny proces hosta (PR_SET_NO_NEW_PRIVS oraz ścisłą listę dozwolonych wywołań BPF seccomp) i uruchamia dockerd wewnątrz bezpiecznego jądra przestrzeni użytkownika gVisor (Sentry) firmy Google. Wszystkie możliwości UID 0 działające w gościu, montowania overlayfs (ext4lite) oraz reguły iptables pozostają ściśle odizolowane wewnątrz procesu piaskownicy.

 1# 1. Zainstaluj rungv w przestrzeni użytkownika (prekompilowane wydanie z atestacją SLSA lub go install)
 2# Opcja A: Zweryfikuj i rozpakuj prekompilowany plik binarny z atestacją SLSA z GitHub Releases
 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# Opcja B: Lub zainstaluj bezpośrednio ze źródeł za pomocą Go
 9CGO_ENABLED=0 GOBIN=~/.local/bin go install z.gfire.dev/gvx/cmd/rungv@latest
10
11# 2. Uruchom docker:dind wewnątrz gvx z jednoplikowym woluminem ext4lite
12#    Przekierowuje ~/.gvx/docker.sock (tryb 0600) na port 2375 gościa
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. Skieruj standardowy CLI Dockera oraz Docker Compose na gniazdo w przestrzeni użytkownika
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# Wynik: overlayfs Alpine Linux ... (containerized)
26
27docker build -t app:test .
28docker run --rm app:test
29docker compose up -d

Dlaczego standardowe narzędzia rootless zawodzą w ograniczonych piaskownicach

Gdy inżynier lub autonomiczny agent programistyczny potrzebuje uruchomić docker build, docker run lub docker compose wewnątrz utwardzonego kontenera lub wielodostępowego runnera, standardowe obejścia zawodzą na pięciu odrębnych barierach jądra.

1. Brak gniazda demona Dockera na hoście

Polecenie docker ps lub docker build kończy się natychmiast niepowodzeniem, gdy hostowe /var/run/docker.sock nie jest zamontowane w środowisku:

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

2. PR_SET_NO_NEW_PRIVS blokuje sudo i pomocników setuid

Utwardzone kontenery (securityContext.allowPrivilegeEscalation: false w Kubernetes, --security-opt=no-new-privileges w Dockerze lub piaskownice exebox / bwrap) ustawiają bit Linuksa PR_SET_NO_NEW_PRIVS w drzewie procesów. Próba eskalacji przez sudo lub binaria setuid (newuidmap, newgidmap, fusermount3) kończy się niepowodzeniem:

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. Polityki seccomp i sysctl blokują unshare(CLONE_NEWUSER)

Narzędzia reklamowane jako „rootless” (Rootless Docker, Podman, Buildah, nerdctl, bubblewrap) nadal opierają się na przestrzeniach nazw użytkownika Linuksa (CLONE_NEWUSER). Zawodzą one zawsze, gdy:

  • Domyślny profil seccomp zewnętrznego kontenera blokuje unshare(2) lub clone(CLONE_NEWUSER).
  • Ubuntu 24.04+ włącza kernel.apparmor_restrict_unprivileged_userns = 1.
  • Węzły Debian lub RHEL ustawiają kernel.unprivileged_userns_clone = 0 lub user.max_user_namespaces = 0.
  • newuidmap nie może zapisać /proc/self/uid_map, ponieważ aktywne jest PR_SET_NO_NEW_PRIVS.

Powstające błędy wyglądają następująco:

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. mount(2) jest zablokowane bez hostowego CAP_SYS_ADMIN

Nawet jeśli środowisko zezwala na CLONE_NEWUSER, zamontowanie świeżego /proc wewnątrz zagnieżdżonej przestrzeni nazw montowania kończy się niepowodzeniem, gdy zewnętrzny kontener ukrył poddrzewa /proc (/proc/acpi, /proc/kcore, /proc/keys) lub usunął mount(2) z seccomp:

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. Brak /dev/fuse uniemożliwia działanie fuse-overlayfs

Nieuprzywilejowane silniki kontenerów, które nie mogą zamontować jądrowego overlayfs, przechodzą na fuse-overlayfs. W minimalnych kontenerach lub podach Kubernetes bez przekazania urządzenia /dev/fuse fuse-overlayfs kończy się natychmiast niepowodzeniem:

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

Jak gvx uruchamia docker:dind w całości w przestrzeni użytkownika

gvx omija wszystkie pięć barier jądra, ponieważ nie prosi jądra Linuksa hosta o tworzenie przestrzeni nazw, montowanie systemów plików ani konfigurowanie interfejsów sieciowych. Zamiast tego rungv uruchamia wewnętrzne jądro Linuksa działające w przestrzeni użytkownika (gVisor Sentry) oraz stos sieciowy działający w przestrzeni użytkownika (netstack) wewnątrz jednego zwykłego, nieuprzywilejowanego procesu.

Uruchamianie nieuprzywilejowanego Docker-in-Docker za pomocą gvx

1. Przestrzenie nazw w pamięci oraz tabela montowań (VFS2)

Gdy dockerd, containerd i runc działają wewnątrz rungv, systrap przechwytuje ich wywołania systemowe i kieruje je do wewnątrzprocesowego jądra Sentry, a nie do jądra hosta:

  • unshare(2), clone(2), pivot_root(2), chroot(2) oraz mount(2) operują na hierarchii zadań Sentry w pamięci oraz tabeli montowań VFS2 (procfs, sysfs, tmpfs, devpts, cgroupfs v2), podczas gdy hostowy proces rungv działa pod Twoim nieuprzywilejowanym UID hosta.

2. Natywny overlayfs na jednoplikowych woluminach ext4lite

Gdy przekażesz -v gvx-docker:/var/lib/docker (lub --mount type=volume,src=/disk/gvx-docker.ext4,dst=/var/lib/docker), gvx montuje wbudowany system plików ext4lite w katalogu /var/lib/docker.

Ponieważ ext4lite implementuje system plików ext4 z dziennikiem JBD2, umieszczony w jednym pliku hosta (gvx-docker.ext4), i obsługuje atrybuty rozszerzone trusted.overlay.*, urządzenia whiteout znakowe (0:0), d_type oraz RENAME_EXCHANGE, dockerd używa w gościu natywnego sterownika pamięci masowej overlayfs. Wieloetapowe operacje docker build współdzielą warstwy Copy-on-Write bez /dev/fuse i bez gwałtownego wzrostu zużycia miejsca na dysku, typowego dla zapasowego sterownika vfs Dockera.

3. Mostek docker0 w przestrzeni użytkownika oraz iptables-legacy

Gdy docker:dind startuje wewnątrz gvx:

  • dockerd w gościu tworzy most docker0 (172.17.0.1/16), podłącza pary wirtualnych veth dla zagnieżdżonych kontenerów i programuje łańcuchy NAT (DOCKER, POSTROUTING) oraz FILTER z dopasowaniami addrtype i conntrack wewnątrz stosu netstack działającego w przestrzeni użytkownika w gvx.
  • Podanie -p unix:~/.gvx/docker.sock:2375 wiąże hostowe gniazdo domeny Unix (tryb 0600) i przekierowuje połączenia bezpośrednio do portu 2375 dockerd działającego w piaskownicy.

Porównanie z istniejącymi narzędziami kontenerowymi

Możliwość / OgraniczenieStandardowy Docker (dockerd)Rootless Docker / PodmanKaniko / Buildah (nieuprzywilejowane)gvx (rungv + docker:dind)
Wymagany root lub sudo na hościeTakNieNieNie
Działa z PR_SET_NO_NEW_PRIVS=1NieNie (newuidmap zawodzi)CzęściowoTak
Działa, gdy unshare(CLONE_NEWUSER) jest zablokowane (EPERM)Nie dotyczyNieBuildah: Nie / Kaniko: TakTak
Działa bez /dev/fuseTak (używa jądrowego overlay2)Tylko przez wolny sterownik kopiowania vfsTakTak (overlayfs na ext4lite)
Obsługuje docker build (BuildKit)TakTak (jeśli userns jest dozwolone)Tylko budowanie obrazów (bez środowiska uruchomieniowego)Tak
Obsługuje docker run i docker compose upTakTak (jeśli userns jest dozwolone)NieTak
Działa na Windows i macOS bez maszyny wirtualnej z LinuksemNie (wymaga WSL2 / maszyny wirtualnej)Nie (wymaga maszyny wirtualnej z Linuksem)NieTak (whpx, hvf, systrap)

Dokładne wymagania jądra hosta Linux

gvx nie wymaga żadnych podwyższonych uprawnień (CapEff: 0000000000000000) ani przestrzeni nazw użytkownika. Na hoście Linux rungv opiera się wyłącznie na trzech standardowych, nieuprzywilejowanych prymitywach jądra:

  1. ptrace(PTRACE_ATTACH) na własnym wątku potomnym, jednorazowo podczas inicjalizacji: systrap uruchamia lokalny wątek pomocniczy (stub) przy starcie, dołącza się do niego, aby zainicjować wspólny kontekst pamięci, a następnie odłącza się. Jeśli host ustawi kernel.yama.ptrace_scope = 3 (co wyłącza ptrace w całym systemie) lub zablokuje ptrace w zewnętrznym filtrze seccomp, inicjalizacja systrap nie może się zakończyć. (Przy yama.ptrace_scope = 1, czyli domyślnym ustawieniu w Ubuntu i Debian, gvx działa poprawnie, ponieważ śledzenie od procesu nadrzędnego do potomnego jest dozwolone.)
  2. seccomp(SECCOMP_SET_MODE_FILTER): Używane do przechwytywania wywołań systemowych gościa z wątków stub kontenera. Jak opisano w części 3 tej serii, gvx działa zarówno wtedy, gdy dostępne jest SECCOMP_FILTER_FLAG_NEW_LISTENER, jak i wtedy, gdy zwraca EBUSY wewnątrz zagnieżdżonej piaskownicy.
  3. mmap(..., PROT_EXEC, ...): Używane do mapowania segmentów ELF gościa oraz trampoliny pamięci współdzielonej systrap.

Przykład end-to-end: budowanie i testowanie aplikacji wielokontenerowej bez roota

Poniższy skrypt uruchamia kompletny docker build oraz dwukontenerowy test integracyjny (redis + kontener aplikacji) wewnątrz nieuprzywilejowanej sesji użytkownika:

 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# Zbuduj obraz, używając overlayfs na ext4lite
20docker build -t demo-ping:latest .
21
22# Utwórz sieć mostkową, uruchom Redis i sprawdź łączność między kontenerami
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# Wynik: PONG
27
28# Posprzątaj kontenery robocze i zatrzymaj piaskownicę
29docker rm -f cache
30docker network rm demo-net
31kill -INT $DIND_PID

Dalsza lektura w tej serii

  1. gvx: Uruchamianie kontenerów Linux, Dockera i Kubernetes w jednym nieuprzywilejowanym procesie: Przegląd architektury gvx, jego wieloplatformowych silników wykonawczych (whpx, hvf, systrap), VFS2 (erofs, ext4lite, hostfs v2) oraz netstack.
  2. Uruchamianie Dockera, docker build i docker compose bez roota i przestrzeni nazw użytkownika (niniejszy wpis)
  3. Uruchamianie Docker-in-Docker wewnątrz gVisor i zagnieżdżonych piaskownic za pomocą gvx: Jak gvx rozwiązuje problemy z modprobe, iptables-nft, overlay2 na gofer oraz zagnieżdżonym SECCOMP_FILTER_FLAG_NEW_LISTENER (EBUSY) wewnątrz gVisor (runsc).
  4. Szybkie izolowane środowiska Dockera dla rollouts RL agentów AI z gvx: Skalowanie równoległych środowisk SWE-bench oraz rollouts RL dzięki resetom woluminów .ext4 typu copy-on-write trwającym 100 milisekund.