Server-class boxes: staging template for control-plane-managed staging VMs #68

Closed
opened 2026-07-17 15:46:06 +00:00 by dan-claude-bot · 1 comment
dan-claude-bot commented 2026-07-17 15:46:06 +00:00 (Migrated from github.com)

Context

The incubator fleet has a staging-server (M900) hosting staging-box — an Incus VM registered in Coolify as a server and used to deploy staging resources. Today that VM is minted by hand: raw incus launch plus a cloud-init profile (incus/staging-vm.yaml) kept in the private infra repo, entirely outside box. This issue makes server-class staging boxes a first-class box use case.

Design: box mints, rig converges, cast registers

Layering (companion rig issue linked below):

  • box mints the VM from a new staging template (this issue).
  • rig, run inside the box (box shellsudo rig bootstrap workload), hardens sshd and joins the tailnet as tag:server. Box never sees the TS_AUTHKEY; rig holds it in process memory per its own contract.
  • cast registers the box in Coolify by its tailscale IP (out of scope for both repos).

Why box's isolation stack needs no changes: the guest joins the tailnet itself, so the control plane's SSH and all app traffic arrive through a tunnel the guest established outbound. The host-side stack (boxnet ACL, port isolation, box-firewall, loopback-only expose) only ever sees allowed outbound UDP. Isolation becomes a feature: the staging box still can't reach the LAN, the host, or sibling boxes — it is reachable only over the tailnet, which is exactly the staging security posture (tailnet-only, never LAN/public).

What changes

  1. templates/staging/ (data-only):
    • box.env: Debian 13 cloud image, user ops, build-sized resources (the control plane builds on the target): BOX_CPU=4, BOX_MEMORY=8GiB, BOX_DISK=100GiB, plus the two new keys below.
    • user-data.yaml: docker + curl/ca-certificates + preinstall rig (curl … rig/main/install.sh | bash). No tailscale, no sshd, no credentials — that is rig's job at bootstrap time.
  2. Two new template allowlist keys in bin/box:
    • BOX_REQUIRE_VM=1 — the template refuses the container-mode fallback in pick_mode(); staging boxes must be VMs (the trust boundary, and docker-in-guest).
    • BOX_AUTOSTART=1 — sets boot.autostart=true on the instance so staging boxes come back deterministically after a host reboot.
  3. Docs amendment (docs/box-design.md): clarify that "no inbound route to a box" means no inbound via the host's network position — a guest can deliberately join an overlay (tailnet) and invite management in; the server-class template is the sanctioned use of that. Document the snapshot-before-join rule: clone from a pre-rig bootstrap snapshot; post-join clones would duplicate tailnet identity.

Non-goals

  • No changes to boxnet, the ACL, box-firewall, or expose.
  • No credential injection — box's creds-free contract is untouched.
  • No control-plane/Coolify API awareness.

Acceptance

  • box new --name staging-2 --template staging on a KVM host mints a VM with docker + rig preinstalled; on a non-KVM host it refuses with a clear error (no silent container fallback).
  • box shellsudo rig bootstrap workload succeeds interactively.
  • The template allowlist still rejects unknown keys; existing templates are unaffected (new keys optional, absent elsewhere).
  • boot.autostart is set on staging boxes and absent on others.

Companion rig issue: https://github.com/heavy-duty/rig/issues/22

## Context The incubator fleet has a `staging-server` (M900) hosting `staging-box` — an Incus VM registered in Coolify as a server and used to deploy staging resources. Today that VM is minted by hand: raw `incus launch` plus a cloud-init profile (`incus/staging-vm.yaml`) kept in the private infra repo, entirely outside box. This issue makes server-class staging boxes a first-class box use case. ## Design: box mints, rig converges, cast registers Layering (companion rig issue linked below): - **box** mints the VM from a new `staging` template (this issue). - **rig**, run *inside* the box (`box shell` → `sudo rig bootstrap workload`), hardens sshd and joins the tailnet as `tag:server`. Box never sees the `TS_AUTHKEY`; rig holds it in process memory per its own contract. - **cast** registers the box in Coolify by its tailscale IP (out of scope for both repos). **Why box's isolation stack needs no changes:** the guest joins the tailnet itself, so the control plane's SSH and all app traffic arrive through a tunnel the guest established *outbound*. The host-side stack (boxnet ACL, port isolation, box-firewall, loopback-only `expose`) only ever sees allowed outbound UDP. Isolation becomes a feature: the staging box still can't reach the LAN, the host, or sibling boxes — it is reachable *only* over the tailnet, which is exactly the staging security posture (tailnet-only, never LAN/public). ## What changes 1. **`templates/staging/`** (data-only): - `box.env`: Debian 13 cloud image, user `ops`, build-sized resources (the control plane builds on the target): `BOX_CPU=4`, `BOX_MEMORY=8GiB`, `BOX_DISK=100GiB`, plus the two new keys below. - `user-data.yaml`: docker + curl/ca-certificates + preinstall rig (`curl … rig/main/install.sh | bash`). No tailscale, no sshd, no credentials — that is rig's job at bootstrap time. 2. **Two new template allowlist keys** in `bin/box`: - `BOX_REQUIRE_VM=1` — the template refuses the container-mode fallback in `pick_mode()`; staging boxes must be VMs (the trust boundary, and docker-in-guest). - `BOX_AUTOSTART=1` — sets `boot.autostart=true` on the instance so staging boxes come back deterministically after a host reboot. 3. **Docs amendment** (`docs/box-design.md`): clarify that "no inbound route to a box" means no inbound *via the host's network position* — a guest can deliberately join an overlay (tailnet) and invite management in; the server-class template is the sanctioned use of that. Document the **snapshot-before-join** rule: clone from a pre-`rig bootstrap` snapshot; post-join clones would duplicate tailnet identity. ## Non-goals - No changes to boxnet, the ACL, box-firewall, or `expose`. - No credential injection — box's creds-free contract is untouched. - No control-plane/Coolify API awareness. ## Acceptance - `box new --name staging-2 --template staging` on a KVM host mints a VM with docker + rig preinstalled; on a non-KVM host it refuses with a clear error (no silent container fallback). - `box shell` → `sudo rig bootstrap workload` succeeds interactively. - The template allowlist still rejects unknown keys; existing templates are unaffected (new keys optional, absent elsewhere). - `boot.autostart` is set on staging boxes and absent on others. --- Companion rig issue: https://github.com/heavy-duty/rig/issues/22
dan-claude-bot commented 2026-07-18 19:11:03 +00:00 (Migrated from github.com)

Closing as superseded by the thin-template layering decided 2026-07-18 (#81, heavy-duty/rig#31): tenant content (docker, joins, hardening) moves to rig bootstrap roles — the staging posture becomes rig bootstrap staging per rig#31 — and box templates shrink to creds-free seeds. This issue's implementation PR (#69) was closed on the same decision; its mint-time survivors already landed via #82 (BOX_REQUIRE_VM / BOX_AUTOSTART keys + the dynamic template test suite, merged). The staging template itself returns as a thin seed under #81 once rig#31's role exists.

Closing as superseded by the thin-template layering decided 2026-07-18 (#81, heavy-duty/rig#31): tenant content (docker, joins, hardening) moves to rig bootstrap roles — the staging posture becomes `rig bootstrap staging` per rig#31 — and box templates shrink to creds-free seeds. This issue's implementation PR (#69) was closed on the same decision; its mint-time survivors already landed via #82 (BOX_REQUIRE_VM / BOX_AUTOSTART keys + the dynamic template test suite, merged). The staging template itself returns as a thin seed under #81 once rig#31's role exists.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/box#68
No description provided.