server/v0.2.2

opensandbox-group/OpenSandboxserver/v0.2.2Jul 20, 2026by Pangjiping

AI Summary

Major server update introducing multi-tenancy support with breaking changes to Credential Vault enforcement and proxy trust mechanisms.

Key Highlights

  • Credential Vault now requires dns+nft enforcement
  • Secure-access token enforced on server proxy
  • Multi-tenancy provider with file and HTTP backends

Breaking Changes

  • Credential Vault requires dns+nft enforcement
  • X-Forwarded-Proto no longer trusted by default
  • OPENSANDBOX_EGRESS_MITMPROXY_SCRIPT removed from env allowlist

New Features

  • Multi-tenancy provider support
  • Sandbox create latency metrics events
  • Sandbox extensions returned in lifecycle responses
  • Snapshot list exact-name filtering
  • Configurable Docker port range for bridge-mode
  • Docker egress fix for hosts without IPv6 procfs entries

Full Release Notes

## What's New

### ⚠️ Breaking Changes

- **Credential Vault requires `dns+nft` enforcement** — DNS-only egress can no longer carry credential-bound traffic. Migrate to `[egress] mode = "dns+nft"`; `defaultAction: allow` still works but is deprecated and warns. #1136

- **`X-Forwarded-Proto` no longer trusted by default** — the server proxy strips client-supplied forwarded headers and rebuilds them from the connection. Deployments that terminate TLS in front of the egress sidecar must list their proxy addresses in the new `OPENSANDBOX_EGRESS_CREDENTIAL_VAULT_TRUSTED_PROXY_CIDRS`. #1138

- **`OPENSANDBOX_EGRESS_MITMPROXY_SCRIPT` removed from the env allowlist** — the variable let a sandbox point the egress sidecar's `mitmdump -s` at any file under the shared `/opt/opensandbox` emptyDir, enabling code execution in the sidecar and Credential Vault plaintext access. Requests carrying it are now rejected. Ship custom addons in a purpose-built egress image via `ENV`. #1194

### 🔒 Security

- **Secure-access token now enforced on the server proxy** — the lifecycle proxy at `/sandboxes/{id}/proxy/{port}/...` previously skipped API-key checks and never validated the `OpenSandbox-Secure-Access` header (CVSSv3.1 ~7.5). Both HTTP and WebSocket paths now require a matching header (constant-time compare) and strip the token before forwarding. #1191

- **Sandbox pods no longer mount a service-account token** — the `kubernetes.service_account` config is removed and `automountServiceAccountToken: false` is set on both providers. Nothing in the sandbox consumed the token. Operators depending on the SA's `imagePullSecrets` should move `serviceAccountName` into their agent-sandbox template. #1227

- **21 CVE-remediating dependency bumps** — `pydantic-settings`, `python-multipart`, `starlette`, `cryptography`, `pyjwt`, `undici`, `js-yaml`. Direct deps pinned in `pyproject.toml`, transitives via `[tool.uv] constraint-dependencies` and `pnpm.overrides`. #1145

### ✨ Features

- **Multi-tenancy provider with file and HTTP backends** — sandboxes and snapshots can be isolated per tenant via K8s namespaces. Ships a file provider (TOML with mtime hot-reload) and an HTTP provider (per-key TTL cache + singleflight). Tenant context is propagated per request through auth middleware; unavailable providers return 503. Closes #972 #497. #1184

- **`sandbox.create` latency reported via metrics events** — new `POST /v1/metrics/events` accepts fire-and-forget `MetricsEvent` payloads and optionally records them as an OTEL histogram (`opensandbox.sandbox.create.duration`). Python/Go/JS SDKs already hook this on every create. Configure via `[otel]`. #1307

- **Sandbox extensions returned in lifecycle responses** — `CreateSandboxResponse`, `getSandbox`, and `listSandboxes` now surface `opensandbox.extensions.*` back to callers, matching the values set at create time. K8s reads from pod annotations; Docker persists them as container labels. Closes #1060. #1112 #1167

- **Snapshot list supports exact-name filtering** — `GET /snapshots` accepts an optional `name` query parameter, honored through the API, service, and SQLite repository while preserving pagination. Exposed in the Python, JavaScript, Go, C#, and Kotlin SDKs. Closes #1205. #1301

- **Configurable Docker port range for bridge-mode sandboxes** — new `[docker] port_range_min` / `port_range_max` fields (default `40000-60000`) narrow host-port allocation so multiple bridge-mode sandboxes can expose distinct execd ports (host mode always binds 44772). #1182

- **`OPENSANDBOX_EGRESS_POLICY_FILE` and `OPENSANDBOX_EGRESS_SANDBOX_ID` now allowlisted** — sandbox creates can opt into egress policy-file persistence and per-sandbox OTLP attribution (OSEP-0010) via the standard `env` field. #1190 #1335

- **JDK trust store hardened for MITM CA** — `bootstrap.sh` now discovers every JDK on the system and imports the egress MITM CA into each `cacerts` via `keytool`. Fixes PKIX failures on tarball / Alpine / distroless JDKs when using the credential proxy. Idempotent and best-effort. Closes #1201. #1229

### 🐛 Bug Fixes

- **Docker `listSandboxes` no longer flakes on concurrent deletion** — docker-py's high-level `containers.list()` does a follow-up inspect per match, so a sibling cleanup mid-listing failed the whole call with 500. The service now uses the low-level `docker_client.api.containers(...)` summary endpoint and maps `NotFound` from single-container lookups to 404. List view drops `entrypoint`, exit code, and `FinishedAt` fidelity (callers needing those must use `getSandbox`). #1342

- **K8s informer honors `informer_resync_seconds`** — periodic full resync was silently disabled while watch mode was active. Watch streams are now capped by the remaining resync interval. Fixes #1322. #1323

- **`networkPolicy` rejected in pool mode** — combining `extensions.poolRef` with a `networkPolicy` is unsupported; requests are now rejected at schema validation with a defense-in-depth guard in `BatchSandboxProvider`. Closes #1315. #1319

- **Diagnostics scoped to the tenant namespace** — K8s pod/log/event lookups use the request-scoped tenant namespace instead of the server default. Non-tenant requests unchanged. Closes #1304. #1305

- **`use_server_proxy` preserves the API mount prefix** — the API is mounted both bare and under `/v1`, but proxied endpoints were derived from `request.base_url` and dropped the prefix, breaking ingress deployments that path-split `/v1/*`. The rewritten endpoint now keeps the prefix; `server.eip` is still respected verbatim. #1297

- **Renewed Docker sandbox expirations survive restart** — timers were rebuilt from immutable container labels, so a renewed sandbox reverted to its original TTL. Renewals now write an override into the file-backed metadata store; old callbacks that observe a renewal rearm cleanly. Closes #1200. #1267

- **PVC `readOnly` propagated to the volume source** — the flag was only applied to the mount, not to `persistentVolumeClaim.readOnly` on the pod spec. Both are now aligned. Closes #545. #1246

- **Fail fast for missing `poolRef` pools** — `extensions.poolRef` is validated against the Pool CRD before creating a `BatchSandbox`, returning `KUBERNETES::POOL_NOT_FOUND` instead of failing downstream. Fixes #1175. #1176

- **Docker egress no longer crashes on hosts without IPv6 procfs entries** — `egress.disable_ipv6=true` unconditionally injected `net.ipv6.conf.*.disable_ipv6` sysctls. The injection is now skipped when the required procfs knobs are missing. Fixes #1168. #1172

- **Event loop no longer blocked during Docker image pull** — provisioning runs on a daemon thread with an `asyncio.Future` so health checks stay responsive. New `timeout_graceful_shutdown` config (default 5s) makes SIGINT actually exit. #1171

- **K8s diagnostics logs endpoint works on multi-container pods** — `read_namespaced_pod_log` was called without a `container` argument, so every standard sandbox (with `egress` sidecar + `execd-installer` init) returned 500. The endpoint now defaults to the `sandbox` container, accepts `?container=` for sidecar/init logs, and maps K8s errors to structured 400/403/404/502. Fixes #1150. #1151

### 📦 Misc

- **`renew_intent` marked stable** — OSEP-0009 is out of experimental; drops `[EXPERIMENTAL]` / 🧪 markers from config docstrings, example TOML, startup logs, and `configuration.md`. No behavior change. #1350

- **Removed unused `PendingSandbox` machinery and fixed the spec** — Docker and K8s both provision synchronously; the async worker, related helpers, and their tests were dead code (−357 lines). The `POST /sandboxes` 202 response description in `specs/sandbox-lifecycle.yml` also described a `Pending → Running` polling flow that was never implemented — now corrected. #1178

- **GHCR image publishing** — component images (server, k8s, execd, egress, ingress) are now published to GitHub Container Registry alongside Docker Hub and Aliyun. Refs #1160. #1161

- **Package metadata and docs point at `opensandbox-group/OpenSandbox`** — user-facing links across README, docs, Helm chart, examples, and `server`/`cli` `pyproject.toml` updated. No package rename. #1197 #1140

- **Auto-bumps** — `execd` v1.0.20 → v1.0.21 (#1144 #1279); `egress` v1.1.3 → v1.1.4 (#1142 #1278).

## 👥 Contributors

Thanks to these contributors ❤️

- @1363653611
- @bcho
- @FAUST-BENCHOU
- @ferponse
- @GreenShadeZhang
- @Gujiassh
- @hellomypastor
- @jianpingpei
- @jwx0925
- @Pangjiping
- @ruirui6946
- @shenshouer
- @tea-artist

---

- PyPI: opensandbox-server==0.2.2
- Docker Hub: opensandbox/server:v0.2.2
- Aliyun Registry: sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/server:v0.2.2
- GHCR: ghcr.io/opensandbox-group/server:v0.2.2