Skip to main content
The Tower is a raw disk image that boots into a configured, self-governing Naive region. It is built once with Packer and is usable three ways from the same artifact: bare metal, a local QEMU VM, and an AWS AMI.

Build — Packer + Ubuntu 24.04 autoinstall

image/tower.pkr.hcl drives a QEMU build of x86_64 Ubuntu 24.04 live-server via autoinstall (ds=nocloud-net, serving image/http/{user-data,meta-data}). It boots UEFI/q35 with OVMF and emits a raw disk. KVM makes it fast on Linux CI; macOS has no KVM, so a local build runs under QEMU TCG (slow) — the authoritative build/boot gate is Linux CI.

The GPT layout

The autoinstall lays down a GPT disk (http/user-data):

Provisioning scripts

Packer runs the provisioners in order:
  • 10-docker.sh — Docker CE + Compose v2 + gVisor runsc, registered with Docker and with containerd’s SystemdCgroup=true (the well-known fix so runsc containers start on a systemd-cgroup host). default-runtime stays runc; runsc is the sandbox for the agent runtime.
  • 20-tpm-luks.sh — installs the firstboot LUKS enroller (see below); it does not seal at build time.
  • 30-tailscale.sh — installs Tailscale but leaves it OFF (the human admin plane only; the owner runs tailscale up post-claim if wanted).
  • 40-region.shdocker loads the preloaded image tarballs and installs compose.tower.yaml + the systemd units.
  • 50-firstboot.sh — installs the firstboot orchestrator and seals the build (drops the Packer passwordless-sudo, cloud-init clean, truncates machine-id so the next boot is a true firstboot).

Firstboot — sealing to the TPM

The seal must bind to the deployment’s TPM (swtpm in a local VM, NitroTPM on AWS, a discrete/fTPM on metal), so it happens at first boot on the target, never in the build. naive-firstboot.service (ordered Before=naive-region.service, guarded by a marker file) runs once:
  1. /data → LUKS2 + TPM auto-unlock. A transient key creates the LUKS2 header, then systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7 binds unlock to PCR 7 (Secure Boot state). crypttab is written for auto-unlock on every subsequent boot — no passphrase. PCR 11 (kernel/initrd) is a beta hardening step.
  2. An owner recovery key is generated with systemd-cryptenroll --recovery-key and printed to the console once. It is the break-glass path if TPM state changes; the operator records it out-of-band.
  3. Vault root + config are written onto the now-encrypted /data (/data/vault/root.key, a per-Tower GOVERNOR_TOKEN in /data/config/naive.env, an empty runtime-tokens.json).
Then naive-region.service runs docker compose -f /opt/naive/compose.tower.yaml up -d — the production topology: the preloaded images, vaultd:tower (reveal compiled out), the real runsc agent-runtime, state on the LUKS/TPM /data, and api/gateway published to guest loopback only (no inbound WAN).
compose.tower.yaml is what boots on the image (preloaded tags, production isolation). image/compose.yaml is the dev/CI-topology compose (build contexts
  • test scaffolding) used by the topology test and the compose quickstart.

Run it locally — QEMU + swtpm

scripts/run-vm.sh boots the raw image under QEMU with a software TPM (swtpm) and OVMF, using KVM when available or TCG otherwise, with the api / gateway ports forwarded. Because swtpm is a real TPM 2.0 to the guest, the cryptenroll PCR-7 seal and vault unlock run exactly as they will on hardware. See the local VM tutorial.

Publish to AWS — NitroTPM

image/publish-ami.sh turns the same raw image into an AMI:
It uploads the raw to S3, runs ec2 import-snapshot (Format=RAW), waits for completion, then:
--boot-mode uefi --tpm-support v2.0 is what wires NitroTPM to the instance, so firstboot’s PCR-7 seal and vault unlock run unmodified on Nitro UEFI instances (m5 / c5 / m6i). No inbound security-group rules are needed for the region itself. See the AWS tutorial.
build-tarballs.sh (docker save region + vaultd:tower + Postgres) and publish-ami.sh are ready. The end-to-end publish additionally requires the account’s vmimport service role and a naive-tower.raw produced by the CI Packer job (local macOS is TCG-only).