GoSuda

gvx: Rularea containerelor Linux, Docker și Kubernetes într-un singur proces neprivilegiat

By Lemon Mint views ...

Introducere: containere securizate fără root sau daemoni pe gazdă

Rularea containerelor Linux, a comenzii docker build sau a Kubernetes pe o mașină de dezvoltare, un runner CI sau un nod cloud multi-tenant impune, de regulă, un compromis incomod în materie de securitate:

  • Pe Linux, runtime-urile OCI standard (runc, crun, runsc) necesită fie privilegii root, fie expunerea /var/run/docker.sock al gazdei, fie --privileged (CAP_SYS_ADMIN), fie namespace-uri de utilizator (unshare(CLONE_NEWUSER)), care extind suprafața de atac a kernel-ului gazdei.
  • Pe Windows și macOS, motoarele de containere (Docker Desktop, Podman Machine, Lima, OrbStack) pornesc un kernel complet de mașină virtuală Linux în WSL2, Hyper-V sau Virtualization.framework și rulează un daemon persistent în fundal în interiorul acestei VM.

gvx (CLI-ul rungv și SDK-ul Go z.gfire.dev/gvx/pkg/gvx) oferă o alternativă sigură, fără privilegii. Construit ca un fork, ce poate fi urmărit în amonte, al kernel-ului Linux în user-space Sentry și al stivei de rețea în user-space netstack ale proiectului gVisor al Google, gvx execută binare linux/amd64 și linux/arm64 ELF64 nemodificate, precum și containere OCI, direct în interiorul unui singur proces gazdă neprivilegiat, cu auto-blocare, pe Windows, macOS și Linux.

Garanție de securitate (apărare în profunzime cu trei inele): gvx nu slăbește niciodată granițele de securitate ale sistemului de operare gazdă, nu instalează niciodată drivere de kernel și nu necesită niciodată root, sudo, --privileged pe gazdă sau CLONE_NEWUSER. Înainte de a executa vreo instrucțiune a oaspetelui, gvx își blochează propriul proces gazdă folosind Windows Restricted Tokens și Job Objects, macOS Seatbelt (deny default) sau Linux PR_SET_NO_NEW_PRIVS + BPF seccomp. Toate apelurile de sistem ale oaspetelui, privilegiile virtuale UID 0, montările (VFS2) și tabelele de rețea (netstack) sunt strict confinate în interiorul kernel-ului Go Sentry, sigur din punct de vedere al memoriei.

RuntimeSistem de operare gazdăPornire la receAmprentă de memorieNecesită admin sau sudoDaemon VM în fundalRulare directă a imaginii OCI
gvx (whpx / hvf)Windows (amd64, arm64) și macOS (arm64)~20 ms~30 MiBNuNuDa
gvx (systrap)Windows (amd64, arm64) și Linux (amd64, arm64)~25 până la 65 ms~31 MiBNuNuDa
WSL2 / Docker DesktopWindows și macOS800 până la 2500 ms> 512 MiBDa (configurare inițială)DaNecesită daemon
QEMU (sistem complet)Windows și macOS1500 până la 4000 ms> 256 MiBNuNuNecesită imagine de disc

Pe Windows sau macOS nu pornește niciun kernel de mașină virtuală Linux. Pe gazdă nu rulează niciun daemon în fundal. Pe sistemul de operare gazdă nu sunt necesare privilegii root, sudo, namespace-uri de utilizator (CLONE_NEWUSER), mount(2), drivere TUN/TAP sau CAP_NET_ADMIN. În interiorul acestui proces neprivilegiat, gvx rulează Docker-in-Docker (docker:dind), docker build, docker compose și Kubernetes de înaltă disponibilitate, multi-nod (k3s cu etcd încorporat și CNI Calico).


Ciclul de viață al containerului și izolarea gazdei în trei inele

Fiecare sandbox gvx rulează într-un singur proces gazdă neprivilegiat și aplică un ciclu de viață în trei faze: deschiderea anticipată a tuturor descriptorilor de fișiere și a socket-urilor gazdei necesare, blocarea procesului sistemului de operare gazdă prin pkg/hostlockdown și intrarea în bucla de execuție Sentry din proces.

Ciclul de viață al containerului gvx și auto-blocarea gazdei

Înainte de a transfera controlul către prima instrucțiune a oaspetelui, gvx deschide anticipat descriptorii de fișiere ai straturilor de imagine .erofs din cache, descriptorii de fișiere ai volumelor .ext4, handle-ul *os.Root pentru /workspace și socket-urile de rețea ale gazdei, după care invocă hostlockdown.Apply():

  1. Windows NT (lockdown_windows.go): Apelează AdjustTokenPrivileges cu SE_PRIVILEGE_REMOVED pentru a elimina privilegiile în loc, leagă un token de impersonare restricționat DISABLE_MAX_PRIVILEGE la integritate Low (S-1-16-4096), atribuie procesul unui Job Object anonim cu JOB_OBJECT_LIMIT_ACTIVE_PROCESS = 1 (care blochează crearea oricărui proces copil) și activează DisallowWin32kSystemCalls = 1 prin SetProcessMitigationPolicy.
  2. macOS XNU (lockdown_darwin.go): Setează RLIMIT_NPROC = 0 și aplică un profil Seatbelt (sandbox_init_with_parameters) pornind de la (version 1)(deny default), care blochează process-fork, process-exec* și toate deschiderile de fișiere noi ale gazdei.
  3. Linux (lockdown_linux.go + pkg/gvx/syscallfilter_linux.go): Setează PR_SET_NO_NEW_PRIVS și instalează lista de permisiuni BPF seccomp a Sentry pe toate firele de execuție (SECCOMP_FILTER_FLAG_TSYNC), cu reguli EPERM explicite pentru execve, execveat, clone, openat și socket.

Interceptarea apelurilor de sistem multiplatformă și paginarea de 4 KB (pkg/sentry/platform)

Binarele Linux ELF64 presupun o granularitate a paginilor de memorie de 4 KB (0x1000) pentru mapările segmentelor mmap(MAP_FIXED). Totuși, VirtualAlloc și MapViewOfFile3 din Windows impun alinierea la 64 KB (0x10000), iar macOS pe Apple Silicon (darwin/arm64) folosește pagini gazdă de 16 KB (0x4000). În plus, fiecare sistem de operare necesită un alt mecanism pentru a intercepta instrucțiunile Linux SYSCALL (amd64) și SVC #0 (arm64), fără a porni o mașină virtuală Linux.

gvx rezolvă ambele probleme prin patru backend-uri de execuție care implementează pkg/sentry/platform.Context:

Mecanismele gvx de interceptare a apelurilor de sistem și de paginare de 4 KB

1. Motorul hardware Windows: platform/whpx (WinHvPlatform.dll)

Pe windows/amd64 și windows/arm64, platform/whpx creează o partiție hipervizor ușoară prin WHvCreatePartition, fără a porni kernelul unui sistem de operare oaspete:

  • Decuplorul de paginare 64 KB-4 KB: whpx înregistrează fiecare arenă de 64 MiB din pgalloc.MemoryFile la partiție o singură dată, prin WHvMapGpaRange. Apelurile ulterioare MapFile și Unmap modifică doar tabelele de pagini ale oaspetelui din interiorul partiției, aplicând permisiuni R/W/X exacte de 4 KB, în ciuda granularității de alocare de 64 KB a Windows.
  • Schimb de registre fără API prin CommPage:
    • Pe amd64, un SYSCALL din Ring-3 al oaspetelui intră într-un stub MSR_LSTAR din Ring-0, aflat în interiorul partiției (stubgen_x86.go), care salvează în CommPage, o pagină partajată de 4 KB, registrele GPR ale apelului de sistem rapid (RAX, RDI, RSI, RDX, R10, R8, R9, RIP, RSP) și execută HLT pentru a ieși din WHvRunVirtualProcessor.
    • Pe arm64, un SVC #0 din EL0 al oaspetelui intră în vectorii EL1 (stubgen_a64.go), salvează X0..X8, SP_EL0, ELR_EL1, PSTATE în CommPage și citește de la GPA zero, neasociat, pentru a declanșa o ieșire de memorie către Sentry.

2. Motorul hardware macOS: platform/hvf (Hypervisor.framework)

Pe Apple Silicon (darwin/arm64), platform/hvf rulează firele containerelor în vCPU-urile Hypervisor.framework (hv_vm_create, hv_vcpu_run):

  • Traducere MMU în două etape, de la pagini de 4 KB peste pagini de 16 KB: Stage-2 (hv_vm_map) mapează, o singură dată, bucăți de fișier de memorie de 1 GiB în spațiul de adrese fizice intermediare (IPA), la granularitatea paginilor gazdă de 16 KB, cu HV_MEMORY_READ | HV_MEMORY_WRITE | HV_MEMORY_EXEC. În interiorul vCPU-ului, stub-ul EL1 bluepill (bluepill_arm64.go) configurează TCR_EL1.TG0 = 0b00 (granulă de 4 KB) în TTBR0_EL1 și populează intrările tabelelor de pagini Stage-1 de 4 KB cu permisiuni exacte R-X sau RW- (UXN=1). Deoarece MMU-ul ARM64 impune intersecția permisiunilor Stage-1 și Stage-2, hardware-ul interceptează erorile de permisiune la limitele exacte de 4 KB.
  • Interceptarea apelurilor de sistem: Un SVC #0 din EL0 al oaspetelui este interceptat de VBAR_EL1 din EL1, salvează registrele în CommPage, o pagină partajată de 4 KB, și execută HVC #0 pentru a reveni din hv_vcpu_run către firul Go al Sentry.

3. Rezerva în mod utilizator pentru Windows: platform/systrap (winsystrap)

Atunci când virtualizarea hardware nu este disponibilă (de exemplu, în VM-uri cloud imbricate), winsystrap rulează codul oaspetelui în mod utilizator pe windows/amd64 și windows/arm64:

  • Izolarea hardware prin CR3 / TTBR0_EL1: Firele oaspetelui rulează în subprocese stub izolate, pornite dintr-un executabil generat de 1 KB (stubimage_xplat.go), confinate de un token cu integritate Low și de un Job Object.
  • Patch binar pe loc și IPC winSysmsg: Pe amd64, patcher-ul (patcher_amd64_xplat.go) înlocuiește instrucțiunile syscall (0f 05) de 2 octeți cu salturi jmp rel32 de 5 octeți (sau cu capcane de breakpoint CC 90, atunci când nu este disponibilă o fereastră de 5 octeți), care duc către o insulă cu cale rapidă. Pe arm64, svc #0 este patch-uit cu o ramură B către un slot din insulă. Insula salvează registrele în pagina partajată winSysmsg și se sincronizează cu Sentry prin WaitOnAddress și WakeByAddressSingle, în timp ce capcanele nepatch-uite și erorile de pagină se opresc pe un Windows Debug Object (WaitForDebugEventEx).

4. Motorul Linux în mod utilizator: platform/systrap

Pe linux/amd64 și linux/arm64, systrap rulează fire ale oaspetelui (sysmsg) care interceptează apelurile de sistem ale oaspetelui prin SECCOMP_RET_TRAP (SIGSYS) și patch-uiesc binar punctele syscall într-un trampolin de memorie partajată (sysmsg), sincronizat prin SYS_FUTEX. În paralel, un syscallThread stub (syscall_thread.go) execută operațiile asupra spațiului de adrese (mmap, munmap, clone) prin SECCOMP_RET_USER_NOTIF (_RUN_SECCOMP_LOOP). Atunci când gvx rulează în interiorul unui sandbox gVisor extern (runsc), unde unicul slot SECCOMP_FILTER_FLAG_NEW_LISTENER returnează EBUSY, gvx trece automat syscallThread pe o buclă futex cu memorie partajată (_RUN_SYSCALL_LOOP), după cum este detaliat în Partea 3 a acestei serii.


Cum mapează gvx apelurile de sistem Linux ale oaspetelui către subsisteme virtuale și handle-uri ale gazdei

Odată ce un motor de platformă interceptează un SYSCALL (amd64) sau o instrucțiune SVC #0 (arm64) a oaspetelui și scrie registrele oaspetelui în pagina partajată de 4 KB (CommPage, sysmsg sau winSysmsg), controlul revine la goroutina task-ului Go Sentry (pkg/sentry/kernel/task_syscall.go). În loc să redirecționeze apelurile de sistem Linux către kernel-ul sistemului de operare gazdă, Task.doSyscall() salvează starea originală a registrelor (SyscallSaveOrig()), evaluează eventualele filtre BPF seccomp din oaspete și opririle ptrace, apoi distribuie numărul apelului de sistem prin linux.AMD64 sau linux.ARM64 (pkg/sentry/syscalls/linux/linux64.go, care acoperă ~370 de apeluri de sistem Linux) în patru domenii interne:

Cum mapează gvx apelurile de sistem Linux ale oaspetelui către subsisteme și sistemul de operare gazdă

Domeniu de apeluri de sistemApeluri de sistem Linux reprezentative ale oaspeteluiSubsistem intern gvx (pkg/sentry)Interacțiunea cu granița sistemului de operare gazdă (după hostlockdown)
A. Procese, namespace-uri, montări și sincronizareclone, execve, unshare, setns, mount, umount2, pivot_root, setuid, capset, futex, epoll_wait, pipe2kernel.Task, acreditări virtuale, tabela futex și VFS2 în memorie (procfs, sysfs, cgroupfs v2, tmpfs, overlayfs)Zero apeluri de sistem ale gazdei (100% stare Go în memorie; complet invizibil pentru sistemul de operare gazdă)
B. Memorie virtuală (MM)mmap, munmap, mremap, mprotect, brk, madvise, memfd_createmm.MemoryManager (arbore VMA de 4 KB) și pgalloc.MemoryFileActualizări bidirecționale ale tabelelor de pagini de 4 KB prin WHvMapGpaRange, hv_vm_map + TTBR0_EL1 sau prin mmap/mprotect ale stub-ului
C. Fișiere și stocare (VFS2)openat, pread64, pwrite64, getdents64, statx, renameat2, setxattr, mknod, inotify_add_watchRouterul tabelei de montări vfs.VirtualFilesystem (fsimpl/erofs, fsimpl/ext4lite, fsimpl/hostfs v2)pread/pwrite bidirecționale pe 1 FD .erofs fixat sau pe 1 FD .ext4, sau parcurgeri relative limitate pe *os.Root
D. Rețea și Netfiltersocket, bind, listen, connect, sendmsg, recvmsg, netlink, setsockopt(IPT_SO_SET_REPLACE)pkg/tcpip în user-space (lo, eth0, docker0, veth, vxlan, wireguard, ipset, iptables-legacy)Traficul din interiorul sandbox-ului folosește zero apeluri de sistem ale gazdei; traficul extern se mapează pe net.Dial/net.Listen (-net=nat) sau pe 1 socket UDP (-net=wg)

Atunci când handler-ul returnează (rval, ctrl, err), Task.doSyscallInvoke() convertește orice eroare într-un errno Linux negativ (-ExtractErrno(err, sysno)), scrie valoarea de return înapoi în pagina partajată a registrelor (RAX pe amd64, X0 pe arm64) și reia execuția oaspetelui.


Stocarea (VFS2) și rețeaua în user-space cu dublă cale (netstack)

Toate operațiile de fișiere și de rețea ale oaspetelui sunt deservite în interiorul Go Sentry, prin trei sisteme de fișiere VFS2 și două piste pkg/netbackend:

Motoarele de stocare VFS2 și netstack cu dublă cale ale gvx

Motor de stocareRolul montării în gvxResursa de bază a gazdeiApeluri openat / NtCreateFile ale gazdei la rulareScanări per fișier ale WdFilter.sys (Windows Defender)
fsimpl/erofsStraturi inferioare de imagine OCI, doar citireUn singur fișier <sha256>.erofs per strat00
fsimpl/ext4liteVolume numite (-v name:/path)Un singur fișier <name>.ext4 per volum00
fsimpl/hostfs v2Spațiul de lucru al gazdei (-workspace, -v /host:/guest)Director de gazdă fixat *os.RootO(depth) per deschidere necacheată1 per închidere de fișier al gazdei

De ce contează ext4lite pentru Docker, baze de date și managerii de pachete

Extragerea sau scrierea a zeci de mii de fișiere de container direct pe NTFS (Windows) sau APFS (macOS) declanșează scanări ale filtrelor antivirus (WdFilter.sys), coliziuni de sensibilitate la majuscule și lipsa atributelor extinse POSIX.

pkg/sentry/fsimpl/ext4lite implementează un sistem de fișiere ext4 pur Go în interiorul unui singur fișier al gazdei deschis anticipat (%LOCALAPPDATA%\gvx\cache\volumes\<name>.ext4 pe Windows, ~/Library/Caches/gvx/volumes/<name>.ext4 pe macOS sau ~/.cache/gvx/volumes/<name>.ext4 pe Linux), cu:

  • Blocuri de 4 KiB și grupuri de blocuri dinamice de 16 MiB (meta_bg): Crește dinamic, fără blocaje de alocare a fișierelor sparse (fallocate pe Linux, fcntl(F_PREALLOCATE) pe macOS, FileAllocationInfo pe Windows) și compactează grupurile finale neutilizate la demontarea curată.
  • Jurnal JBD2 în modul ordonat (inode 8): Confirmări atomice ale tranzacțiilor cu sume de control CRC-32 și blocuri de revocare, plus recuperare fsckLite.
  • Suport pentru stratul superior overlayfs: Implementează atributele extinse trusted.overlay.* și security.capability, whiteout-uri de dispozitive caracter (0:0), raportarea intrărilor de director d_type și RENAME_EXCHANGE.

Sarcini practice: Docker Engine, Kubernetes (k3s + Calico) și SDK-ul Go

Instalarea rungv: versiuni precompilate atestate SLSA sau go install

Puteți descărca o arhivă rungv precompilată pentru linux/amd64, linux/arm64, darwin/arm64, windows/amd64 sau windows/arm64 de la GitHub Releases (fiecare construită de pipeline-ul de lansare SLSA, cu un pachet de proveniență Sigstore și SHA256SUMS), sau o puteți instala direct din sursă, cu go install:

1# Opțiunea A: Descarcă o versiune precompilată și verifică proveniența construcției SLSA
2gh attestation verify rungv-v0.2.0-linux-amd64.tar.gz --repo gfire-sigs/gvx \
3  --signer-workflow gfire-sigs/gvx/.github/workflows/release.yml \
4  --source-ref refs/tags/v0.2.0 --deny-self-hosted-runners
5shasum -a 256 -c SHA256SUMS --ignore-missing
6tar -xzf rungv-v0.2.0-linux-amd64.tar.gz
7
8# Opțiunea B: Instalează direct prin Go (Linux / Windows)
9CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest

Pe macOS (darwin/arm64), arhiva precompilată este deja semnată cu drepturile com.apple.security.hypervisor (xattr -d com.apple.quarantine rungv elimină carantina browserului). Dacă construiți local cu go install sau go build pe macOS, semnați binarul cu codesign --sign - --entitlements entitlements.plist --force $(which rungv).

1. Rularea unui Docker Engine neprivilegiat (docker:dind)

Urmând docs/docker-engine.md, puteți rula un container docker:dind nemodificat în rungv pe Windows, macOS sau Linux și îl puteți controla cu CLI-ul docker al gazdei:

 1# 1. Creează directorul gazdei pentru socket-ul Unix redirecționat
 2mkdir -p ~/.gvx
 3
 4# 2. Pornește docker:dind într-un sandbox gvx cu un volum ext4lite
 5rungv run \
 6  -p unix:~/.gvx/docker.sock:2375 \
 7  -p 127.0.0.1:9000-9019:9000-9019 \
 8  -v gvx-docker:/var/lib/docker \
 9  -e DOCKER_TLS_CERTDIR= \
10  docker:dind --tls=false

Într-un al doilea terminal, indicați CLI-ul docker al gazdei către socket-ul sandbox-ului:

1docker context create gvx --docker host=unix://$HOME/.gvx/docker.sock
2docker context use gvx
3
4docker info --format '{{.Driver}} {{.OperatingSystem}}'
5# Ieșire: overlayfs Alpine Linux ... (containerized)
6
7docker build -t myapp ./app
8docker compose up -d

2. Rularea Kubernetes (k3s + Calico) în sandbox-uri neprivilegiate

Deoarece gvx implementează /dev/kmsg, cgroup v2 (memory.max cu OOM killer și cpu.max cu limitarea lățimii de bandă), /proc/diskstats, dispozitivele netlink wireguard și vxlan, ipset și iptables-legacy (addrtype, conntrack, statistic, recent), o imagine rancher/k3s nemodificată ajunge în starea Ready în interiorul unui singur proces rungv:

 1mkdir -p ~/.gvx/k3s
 2
 3rungv run \
 4  -e IPTABLES_MODE=legacy \
 5  -p 127.0.0.1:6443:6443 \
 6  -p 127.0.0.1:30000-30019:30000-30019 \
 7  -v k3s:/var/lib/rancher \
 8  --mount type=bind,src=$HOME/.gvx/k3s,dst=/output \
 9  rancher/k3s:v1.31.4-k3s1 server \
10  --disable=traefik,servicelb \
11  --write-kubeconfig=/output/kubeconfig.yaml \
12  --write-kubeconfig-mode=644

Puteți, de asemenea, să uniți mai multe noduri gvx prin WireGuard (flannel-backend=wireguard-native sau o rețea mesh wg0), cu disponibilitate ridicată etcd încorporat, sau să rulați Calico (calico-vxlan.yaml cu FELIX_IPTABLESBACKEND=Legacy) pentru a aplica NetworkPolicy Kubernetes complet în user-space.

3. Încorporarea containerelor Linux în Go prin z.gfire.dev/gvx/pkg/gvx

Aplicațiile Go pot încorpora containere Linux direct, fără un daemon extern de containere:

 1package main
 2
 3import (
 4	"context"
 5	"fmt"
 6	"os"
 7
 8	"z.gfire.dev/gvx/pkg/gvx"
 9)
10
11func main() {
12	ctx := context.Background()
13	sb, err := gvx.New(ctx, gvx.Config{
14		Platform:      gvx.PlatformAuto,
15		NetworkMode:   gvx.NetModeNAT,
16		OCIImage:      "alpine:3.20",
17		WorkspacePath: "./repo",
18		Argv:          []string{"/usr/bin/git", "status"},
19		Stdout:        os.Stdout,
20		Stderr:        os.Stderr,
21	})
22	if err != nil {
23		fmt.Fprintf(os.Stderr, "init sandbox: %v\n", err)
24		os.Exit(1)
25	}
26	defer sb.Close()
27
28	exitCode, err := sb.Run(ctx)
29	if err != nil {
30		fmt.Fprintf(os.Stderr, "run sandbox: %v\n", err)
31		os.Exit(1)
32	}
33	os.Exit(exitCode)
34}

Lecturi suplimentare din această serie

  1. gvx: Rularea containerelor Linux, Docker și Kubernetes într-un singur proces neprivilegiat (această postare)
  2. Rularea Docker, docker build și docker compose fără root sau namespace-uri de utilizator: Cum se pornește docker:dind în sandbox-uri restricționate, unde sudo, unshare(CLONE_NEWUSER), mount(2) și /dev/fuse sunt blocate.
  3. Rularea Docker-in-Docker în gVisor și în sandbox-uri imbricate cu gvx: De ce apt-get install docker.io și runsc imbricat eșuează în interiorul gVisor din cauza a cinci bariere ale kernel-ului și cum rulează gvx un docker:dind imbricat, folosind o buclă futex cu memorie partajată.
  4. Medii Docker izolate și rapide pentru rollout-urile RL ale agenților AI cu gvx: Scalarea rollout-urilor SWE-bench și ale învățării prin întărire a agenților cu sute de instanțe docker:dind neprivilegiate, în paralel, și resetări de volume .ext4 copy-on-write de 100 de milisecunde.