Skip to content

v0.10.12.

Complete release notes · 2026-09-27. Read upgrade instructions and runtime limits before deploying.

The follow-up audit patch: a member-level host-execution hole closed, the Volumes page made honest, and the update path made race-proof (r380–r396).

Security

  • The static build pack was ungated host command execution for any member (r380). buildPack: 'static' runs the repository's install and build commands on the panel host (sh -c in engine/builders/staticSite.ts) — the same reach as a PM2 service — but the host-privilege gate (H-3) enumerated only PM2, compose, lifecycle hooks and docker-socket templates. A plain member could create a type: 'docker' service with build: { buildPack: 'static', buildCmd: 'curl … | sh' } and the deploy pipeline executed it as the panel user. The gate now covers buildPack on create, on PATCH (merged with the stored definition, so it cannot be reached one field at a time), on clone, on bundle import (whose buildPack was also cast unvalidated), and on every deploy of a stored definition. The build-pack selector marks static admin-only in the UI.
  • Orchestrator stack names reached filesystem joins unvalidated: GET /v1/orchestrators/:name/stacks/:stack passed the segment straight into join(STACK_ROOT, name) + readFileSync; names are now shape-checked like every other module (r381).

Fixed

  • The dashboard reported every remote-node service as down (r382). The health probe inspected the panel's own Docker daemon; a node-pinned runtime's container lives on the node, so containerIp answered null and the health column showed red for services running fine. The panel now reports healthy: null ("on node") for node-pinned runtimes — the same guard Doctor received in r228 — and the "all systems operational" banner counts only explicit failures. The netns probe image is also pinned instead of :latest (the supply-chain rule every other helper image follows).
  • Volume delete and prune reported success while nothing was deleted (r383). docker volume rm fails silently for a volume still mounted by an orphaned container, and the route swallowed it: the operator clicked Prune, got deleted: N, and the volumes were still on disk — the r351 slug-reuse flow explicitly directs operators there. Both routes now verify the removal landed (volumeExists) and answer 409/skip with a log line when it did not. The Volumes listing also stopped re-reading the entire services/databases/attachments tables per volume (3V full-table reads before the first size measurement).
  • The one-click system export raced its own cleanup (r384). The handler unlinked the archive — the database, the master key and the .env, every secret on the instance — in a finally that ran in the same tick as reply.send(), before the read stream had necessarily opened the file: intermittent ENOENT downloads on Linux, and on Windows (where an open file cannot be unlinked) a permanent secret-bearing file left in the data dir. Cleanup now runs on the stream's close, and crash-orphaned export artifacts (ninedeploy-backup-*, _db-*, _env-*, _meta-*) are swept by hourly housekeeping.
  • A stalling registry hung the deploy pipeline and the registry lock forever (r385). docker login was the one engine subprocess without a timeout; a registry that accepted TCP and never answered left the deployment building and every later deploy on the same registry queued behind the never-released lock. Login now times out after 120 s, kills the child, and fails the deploy with the registry named.
  • The update path was racy at three layers (r386–r389). Two POST /update-start in the same milliseconds both passed the running-check and both spawned an updater (check-then-write); install.sh had no lock at all, so a self-update could interleave with an operator's SSH run (rm -rf/tar -x/pnpm install on one tree, two rollback manifests); and the confirm dialog fired onConfirm on every click. startSelfUpdate now claims an exclusive-create lock (stolen after 10 s if the claimer died), install.sh holds a per-install-dir flock (30-minute wait), and ConfirmDialog disables after the first click. Also in this set: a fresh install.sh --docker run as a non-root user no longer fails its own compose provenance check (sudo's env_reset stripped the dummy variables — they travel in an --env-file now), an explicit --bare-metal refuses to build a second panel over a docker install, and a failed launch no longer reports "The updater exited null".
  • Stopping the panel killed deploys the worker was cleanly finishing (r390). The deploy worker's shutdown grace is 60 s (in-flight deploys checkpoint-cancel inside it), but the systemd unit SIGKILLed the cgroup at the default 30 s — every panel update or restart that landed mid-deploy left the row building and its service undeployable until the 45-minute staleness sweep. TimeoutStopSec=90 in the unit and stop_grace_period: 90s in the compose file honor the designed window.
  • Monitoring showed the master host's numbers as a remote node's (r391). Selecting a node card relabeled the overview cards "Node CPU / Node Memory / Node Storage" while /v1/stats only ever returns the panel host's data — an operator diagnosing a node read the master's disk and RAM. The cards stay labeled "Host" and an explicit note says what the selection does and does not show.
  • Service settings bled across services (r392). Navigating from /services/1?tab=settings to /services/2?tab=settings keeps the route mounted, and the Settings/Network/Volumes tabs hold per-service useState (limits, replicas, preview pattern, target node): service B's form opened with service A's values and Save wrote them onto B. The tabs are now keyed by service id.
  • Copy buttons silently did nothing on plain-http panels (r393). navigator.clipboard exists only in secure contexts, and the panel is routinely reached at http://<server-ip>:<port>; the webhook secret, the agent enrolment command, the password-reset link and the Hub compose copy all no-op'd there (Hub even showed "Copied ✓"). A legacy execCommand fallback restores every copy button, and the one-time-secret reveals no longer risk being dismissed unsaved.
  • Assorted (r394–r396). A local stack's status view showed only its first service (the fixed compose parser from listStacks is now shared); insights refresh — a synchronous full repo clone on the request path — gained the same rate limit as the analysis route; a failed database-limit restart marks the row error instead of leaving it running with no container; volume-snapshot rows on the Backups page disable Restore with a hint instead of clicking dead; volume-restore failures render an error card; the Deploys tab's hover-only actions are visible on keyboard focus; and the Services list's start/stop buttons actually disable while toggling (the state was written but never set).

Source CHANGELOG.md at 6046cfe8