Deploying the IronClaw control-plane
June 18, 2026 · View on GitHub
This directory holds the installer, service definitions, and notes for running the control-plane host. The hardened runtime has three external dependencies that are intentionally not vendored into the stdlib-only skeleton; install them on the host before running in production.
Quick start
sudo deploy/install.sh # build + install binaries, config, and the service
install.sh is a real, idempotent installer: it builds the
control-plane and ironctl, installs them under /usr/local/bin, provisions
/etc/ironclaw/ironclaw.env (0600; generates an API token on first run and
preserves it on re-install), and installs + enables the host service —
ironclaw.service (systemd/Linux) or
io.ironclaw.controlplane.plist
(launchd/macOS). Override defaults via the IRONCLAW_* env vars documented at the
top of the script.
Components
-
containerd + gVisor (
runsc) — every sandbox runs under gVisor via theio.containerd.runsc.v1runtime. gVisor interposes a user-space kernel between the sandbox and the host kernel, shrinking the syscall attack surface. The sandbox OCI spec setsnetwork=none, drops all capabilities, setsno_new_privs, runs non-root in a user namespace, and uses a read-only rootfs with only a small writable/workspace(seeinternal/host/isolation).Rootfs provisioning. The pinned
/sandboximage's filesystem is unpacked into each bundle'srootfs/by theContainerdProvisioner(internal/host/isolation/provisioner.go), which shells out to containerd'sctrCLI — no extra image toolchain, no Go dependency added to the stdlib-only tree. The image is pulled and unpacked once host-side (the sandbox isnetwork=none) into a shared, digest-keyed rootfs and reused read-only across sessions;Launchkeeps a rootfs post-condition so a missing/broken provisioner fails loudly (ErrRootfsMissing) rather than starting an empty sandbox. Pre-pull the pinned image withctr -n ironclaw images pull <image>(see install.sh). Hosts that forbidctr images mount(needs hostCAP_SYS_ADMIN) can plug in an extract-based unpack or a bind/reflink materializer via the provisioner's options.Durable per-group storage. The rootfs is read-only; the sandbox's writable surface is mounts. Beyond the ephemeral tmpfs default, each sandbox can be given per-group persistent storage (see
SandboxSpec.WorkspacePath/MemoryPath/SharedReadOnlyPathininternal/host/isolation):/workspace— per-group durable scratch, bound rw (replaces the tmpfs)./memory— per-group durable memory, bound rw./shared— a global, read-only shared-assets mount.
All writable mounts carry
nosuid,nodev,noexec;/sharedisro. Lay out the host dirs per agent group and chown them to the sandbox's mapped non-root uid (the distrolessnonroot, uid65532);/sharedis operator-managed and read-only. The isolator creates the rw dirs on launch if absent, but ownership is a deploy responsibility:install -d -m 0700 -o 65532 -g 65532 \ /var/lib/ironclaw/groups/<group>/workspace \ /var/lib/ironclaw/groups/<group>/memory install -d -m 0755 /var/lib/ironclaw/shared # global, read-only to sandboxes -
Tailscale — the control-plane API has no public port. Bind it to the host's tailnet IP and reach
ironctlover the tailnet. A host firewall should drop inbound to the API port on every interface except the Tailscale one. -
The control-plane binary —
cmd/controlplane, run underironclaw.service(systemd) orio.ironclaw.controlplane.plist(launchd). Both read all tunables from the 0600/etc/ironclaw/ironclaw.env, so the unit files stay static and hold no secrets. The control-plane is the trusted host orchestrator (it launches gVisor sandboxes), so the unit is deliberately not self-confined — isolation is applied to the sandboxes, not to this process.
Sandbox image
The /sandbox agent runs from a pinned container image built by
../container/build.sh from
../container/Dockerfile (build context is the repo
root). It compiles ./cmd/sandbox with CGO (the SQLCipher binding) and ships it
on a minimal Debian-slim rootfs as a non-root user; it holds no secrets and
no session key. Pin the resulting digest in the provisioner trust policy
(PinnedDigestPolicy) and in IRONCLAW_SANDBOX_IMAGE:
container/build.sh ghcr.io/your-org/ironclaw-sandbox:1.0 # prints the RepoDigest to pin
Running
The installer enables the service; to run the binary directly for development:
ironclaw-controlplane \
--api-addr "$(tailscale ip -4):8787" \
--model-proxy-socket /run/ironclaw/modelproxy.sock
Then, from a host on the tailnet:
ironctl --addr http://<tailnet-ip>:8787 change submit \
--kind persona --group g1 --by slack:alice
ironctl --addr http://<tailnet-ip>:8787 change pending
ironctl --addr http://<tailnet-ip>:8787 change approve <id> --by slack:admin
Model egress
The sandbox has no network. Its only outbound path is the host model proxy on the
bound unix socket, which enforces a destination allowlist (default:
api.anthropic.com) plus per-session/token rate caps, audit records, and response
secret redaction (internal/host/modelproxy).
See install.sh for the installer and the IRONCLAW_* tunables it
honors.