From 8398d8ed2b683ccebc4b35fc7d96906364583b5b Mon Sep 17 00:00:00 2001 From: wei Date: Wed, 1 Jul 2026 22:44:17 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(daytona):=20upgrade=20to=20v0.189=20?= =?UTF-8?q?=E2=80=94=20pagination=20+=20admin-perm=20+=20native=20isolatio?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compose changes: - 4 daytona image tags: pinned Feb 2026 sha → v0.189.0-amd64 - New init container `grant-admin-perms` grants the daytona-admin API key the full v0.189 permission enum. v0.189 introduces `api_key.permissions` and returns 403 on write ops when the column is empty (pre-existing admin keys carry the migration default `{}`). Guarded by `permissions='{}'` so it fires only on upgrade paths. - Enable native cross-sandbox network isolation via runner env `INTER_SANDBOX_NETWORK_ENABLED=false` (v0.179+). Runner spawns sandbox containers on a dedicated bridge with icc disabled; direct L3 between sandbox IPs is dropped by dockerd. - Remove the `runner-firewall` sidecar (from PR #45). That sidecar's DOCKER-USER iptables rule only fires when the host has br_netfilter loaded, which is not the case on default GCP Ubuntu images. Native v0.189 isolation supersedes it end-to-end. Code changes: - `Client.ListSandboxes` now handles the v0.189 paginated envelope `{items, nextCursor}` (was `[...]` in Feb 2026) and walks the cursor. Falls back to flat-array decoding for backwards compatibility. Uses `?limit=200` (v0.189 max; ignored by older versions). Without the ListSandboxes fix, admin `/api/sandbox_list?wallet=`, owner-scoped `/api/sandbox`, `/api/sessions`, `/api/archive-all` and `archiveRunningOnShutdown` all silently return empty on v0.189. Tested end-to-end on wei-tapp2 (a real tapp deployment, on-chain register + authorizeInvalidator + provider register + smoke test): * fresh v0.189 install → grant skipped, full lifecycle ok * Feb 2026 → v0.189 upgrade with pre-existing sandboxes → grant fires (UPDATE 1), old sandbox survives `class` → `sandboxClass` migration, start/stop still work, new sandbox creates cleanly on v0.189 * ssh-access, toolbox on normal sandbox → 200; on sealed sandbox → 403 * list (owner + admin) → correct after pagination fix * voucher settle → 4 vouchers landed on chain, provider earnings updated * stop→start data persistence → `/home/daytona/marker.txt` survives * cross-sandbox isolation → curl / raw TCP from sandbox B to sandbox A times out (bridge changed from docker0 172.17 to 172.20 with icc=false) * pg_dump / restore rollback path preserved --- docker/sandbox/docker-compose.yml | 93 ++++++++++++++----------------- internal/daytona/client.go | 51 ++++++++++++++--- 2 files changed, 85 insertions(+), 59 deletions(-) diff --git a/docker/sandbox/docker-compose.yml b/docker/sandbox/docker-compose.yml index 3df05d7..d0410a3 100644 --- a/docker/sandbox/docker-compose.yml +++ b/docker/sandbox/docker-compose.yml @@ -75,7 +75,7 @@ services: # Port 3000 is intentionally NOT published. All external API access must go # through the sandbox proxy on port 8080. api: - image: daytonaio/daytona-api@sha256:c1203729b066b7b4724a03973a1c167625b8bef89cc2172557e06871c1e3a082 + image: daytonaio/daytona-api:v0.189.0-amd64 environment: - OTEL_ENABLED=false # Snapshot used when a create request doesn't specify one. Per-env: a @@ -196,7 +196,7 @@ services: privileged: true runner: - image: daytonaio/daytona-runner@sha256:a1de89138d9f549f3313f6896748b36f30d0bead4a0a84ddf79f56a1fbe036b0 + image: daytonaio/daytona-runner:v0.189.0-amd64 environment: - ENVIRONMENT=production - API_PORT=3003 @@ -205,12 +205,13 @@ services: # Image default is true (baked into Dockerfile); must explicitly set # to false to enable Docker CFS/memory cgroup limits on sandbox containers. - RESOURCE_LIMITS_DISABLED=false - # NOTE: `INTER_SANDBOX_NETWORK_ENABLED=false` is the upstream-supported - # way to disable cross-sandbox traffic — but it landed in daytona-runner - # only ~v0.179+ (May 2026). Our pinned image (sha256:a1de89138d..., - # Feb 2026) predates it; setting it here is a no-op on that image. - # Until we upgrade the runner image, cross-sandbox isolation is - # enforced by the `runner-firewall` sidecar service below. + # Native cross-sandbox network isolation (v0.179+). Runner spawns user + # containers on a dedicated bridge with icc disabled; direct L3 between + # sandbox IPs is dropped by dockerd. Verified end-to-end on wei-tapp2: + # docker0 → 172.20.0.0/16 bridge, curl/TCP between sandboxes times out. + # Supersedes the previous `runner-firewall` sidecar that relied on the + # host having br_netfilter loaded (unavailable on default GCP Ubuntu). + - INTER_SANDBOX_NETWORK_ENABLED=false - AWS_ENDPOINT_URL=http://minio:9000 - AWS_REGION=us-east-1 - AWS_ACCESS_KEY_ID=${MINIO_ROOT_USER} @@ -227,48 +228,8 @@ services: privileged: true restart: always - # SECURITY: cross-sandbox network isolation enforcer. - # Shares the runner container's network namespace and inserts an - # iptables rule in the DOCKER-USER chain that drops any packet - # forwarded between two containers on docker0 (the runner's internal - # bridge where all sandboxes live). Reconciles every 60s so the rule - # is restored after runner / dockerd restarts. - # - # docker0 -> docker0 forwarding is exactly cross-sandbox traffic. - # sandbox -> external (egress via eth0) and sandbox -> gateway (INPUT - # to the runner, not FORWARD) are unaffected. - # - # daytona-runner's own iptables persistence loop manages a separate - # set of chains (per-sandbox egress rules); it does not touch - # DOCKER-USER, so this sidecar and daytona-runner coexist cleanly. - # - # Once we upgrade the runner image to v0.179+ this sidecar can be - # replaced by setting `INTER_SANDBOX_NETWORK_ENABLED=false` on the - # runner service. - runner-firewall: - image: alpine:3.22 - network_mode: "service:runner" - cap_add: - - NET_ADMIN - depends_on: - runner: - condition: service_started - command: - - sh - - -c - - | - apk add --no-cache iptables >/dev/null 2>&1 - echo "runner-firewall: enforcing cross-sandbox network isolation on DOCKER-USER" - while true; do - iptables -C DOCKER-USER -i docker0 -o docker0 -j DROP 2>/dev/null \ - || ( iptables -I DOCKER-USER 1 -i docker0 -o docker0 -j DROP \ - && echo "$(date -u +%FT%TZ) inserted DOCKER-USER docker0->docker0 DROP rule" ) - sleep 60 - done - restart: always - proxy: - image: daytonaio/daytona-proxy@sha256:2a4ce1cf1fb1d8f21ac6f49cea17e7ed5c7c591bb4af06b3ba001b7e5446d209 + image: daytonaio/daytona-proxy:v0.189.0-amd64 ports: - "4000:4000" environment: @@ -290,7 +251,7 @@ services: restart: always ssh-gateway: - image: daytonaio/daytona-ssh-gateway@sha256:870b20ea3fdcceb9bc52cf74269fea7f516af1af025034a84bc1a6bf298fd31d + image: daytonaio/daytona-ssh-gateway:v0.189.0-amd64 ports: - "2222:2222" environment: @@ -336,6 +297,38 @@ services: - app-net restart: unless-stopped + # v0.189 initialises the admin api_key with an empty permissions array, + # which returns 403 on every write op (stop/start/create). Grant the full + # v0.189 permission enum once the api has finished migrations. Idempotent: + # the WHERE clause skips the row after the first successful update. + grant-admin-perms: + image: postgres:16-alpine + depends_on: + db: + condition: service_healthy + api: + condition: service_healthy + environment: + PGPASSWORD: ${DAYTONA_DB_PASSWORD} + command: + - psql + - -h + - db + - -U + - ${DAYTONA_DB_USER} + - -d + - ${DAYTONA_DB_NAME} + - -v + - ON_ERROR_STOP=1 + - -c + - | + UPDATE api_key + SET permissions = '{write:registries,delete:registries,write:snapshots,delete:snapshots,write:sandboxes,delete:sandboxes,read:volumes,write:volumes,delete:volumes,write:regions,delete:regions,read:runners,write:runners,delete:runners,read:audit_logs}' + WHERE name = 'daytona-admin' AND permissions = '{}'; + networks: + - app-net + restart: on-failure:5 + daytona-redis: image: redis@sha256:8b81dd37ff027bec4e516d41acfbe9fe2460070dc6d4a4570a2ac5b9d59df065 volumes: diff --git a/internal/daytona/client.go b/internal/daytona/client.go index e4d5c24..26968dc 100644 --- a/internal/daytona/client.go +++ b/internal/daytona/client.go @@ -81,17 +81,50 @@ func (c *Client) GetSandbox(ctx context.Context, id string) (*Sandbox, error) { return &s, json.NewDecoder(resp.Body).Decode(&s) } +// ListSandboxes returns every sandbox visible to the admin key. v0.189+ +// paginates the response as {items, nextCursor}; older versions returned a +// flat array. Handle both shapes and walk the cursor when present. func (c *Client) ListSandboxes(ctx context.Context) ([]Sandbox, error) { - resp, err := c.do(ctx, http.MethodGet, "/api/sandbox", nil) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("daytona ListSandboxes: status %d", resp.StatusCode) + var all []Sandbox + cursor := "" + for { + // v0.189 caps limit at 200; larger values return 400. Pre-v0.189 ignores. + path := "/api/sandbox?limit=200" + if cursor != "" { + path += "&cursor=" + cursor + } + resp, err := c.do(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, err + } + body, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("daytona ListSandboxes: status %d", resp.StatusCode) + } + // v0.189+ envelope. + var paged struct { + Items []Sandbox `json:"items"` + NextCursor string `json:"nextCursor"` + } + if err := json.Unmarshal(body, &paged); err == nil && paged.Items != nil { + all = append(all, paged.Items...) + if paged.NextCursor == "" { + return all, nil + } + cursor = paged.NextCursor + continue + } + // Legacy flat array. + var flat []Sandbox + if err := json.Unmarshal(body, &flat); err != nil { + return nil, fmt.Errorf("daytona ListSandboxes: decode: %w", err) + } + return flat, nil } - var list []Sandbox - return list, json.NewDecoder(resp.Body).Decode(&list) } func (c *Client) StopSandbox(ctx context.Context, id string) error { From 7a7e827a70d57c5efae61aa52282fb2e0997174d Mon Sep 17 00:00:00 2001 From: wei Date: Thu, 2 Jul 2026 21:18:22 +0800 Subject: [PATCH 2/2] feat(compose): wrap runner in sysbox-runc for host-boundary isolation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sets docker-compose `runtime: sysbox-runc` on the runner service. The host dockerd starts runner under sysbox-runc, putting the entire runner (its DinD + all user sandbox containers) inside a user namespace. Container uid 0 maps to a non-root uid on host (verified: `0 362144 65536`), so even a fully privileged user sandbox that escapes its container boundary lands as a non-root uid on host. Preserves the DinD topology, the internal registry, and the v0.189 native cross-sandbox network isolation. Compose diff is one field. Requires sysbox-runc registered on the host dockerd — that side is managed by 0g-tapp gcp-cvm build-gcp-tapp.sh `ENABLE_SYSBOX=1`. Verified end-to-end on wei-tapp2: * runner container Runtime=sysbox-runc, uid_map remapped * user sandbox uid_map also `0 362144 65536` (inherits runner's user-ns) * `sudo mount --bind /` inside sandbox mounts within nested user-ns only * lifecycle (create/stop/start/delete), ssh-access, toolbox all pass * cross-sandbox isolation (icc=false on runner-bridge) still enforced * voucher settlement on-chain still works --- docker/sandbox/docker-compose.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docker/sandbox/docker-compose.yml b/docker/sandbox/docker-compose.yml index d0410a3..c45f83c 100644 --- a/docker/sandbox/docker-compose.yml +++ b/docker/sandbox/docker-compose.yml @@ -197,6 +197,14 @@ services: runner: image: daytonaio/daytona-runner:v0.189.0-amd64 + # Sysbox at the outer boundary: host dockerd starts the runner via + # sysbox-runc so the runner (and its DinD + user sandboxes) live in a + # user namespace. Container uid 0 maps to a non-root uid on host, so + # even a privileged user sandbox escaping to the runner is not host + # root. Preserves the DinD + internal-registry topology unchanged; + # only the outer runtime shifts. Requires sysbox-runc registered on + # the host dockerd (see 0g-tapp gcp-cvm build-gcp-tapp.sh ENABLE_SYSBOX). + runtime: sysbox-runc environment: - ENVIRONMENT=production - API_PORT=3003