> ## Documentation Index
> Fetch the complete documentation index at: https://usenaive.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# The Tower golden image

> How the bootable Tower is built: Packer + Ubuntu 24.04 autoinstall, the GPT layout, firstboot TPM-LUKS sealing with an owner recovery key, preloaded images, the systemd boot chain, and running it locally (QEMU + swtpm) or on AWS (NitroTPM).

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`):

| Partition | Size | Role                                                       |
| :-------- | :--- | :--------------------------------------------------------- |
| **ESP**   | 512M | FAT32 EFI system partition (`/boot/efi`)                   |
| **rootA** | 8G   | the active root filesystem (`/`)                           |
| **rootB** | 8G   | empty A/B slot (for Mender-style updates — beta)           |
| **/data** | rest | installed plaintext, converted to LUKS2 + TPM at firstboot |

### 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.sh`** — `docker load`s 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).

<Note>
  `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.
</Note>

## 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](/docs/self-hosting/tutorial-local-vm).

## Publish to AWS — NitroTPM

`image/publish-ami.sh` turns the **same** raw image into an AMI:

```sh theme={"theme":"css-variables"}
AWS_REGION=us-east-1 S3_BUCKET=my-bucket image/publish-ami.sh image/output/naive-tower.raw
```

It uploads the raw to **S3**, runs `ec2 import-snapshot` (`Format=RAW`), waits
for completion, then:

```sh theme={"theme":"css-variables"}
aws ec2 register-image \
  --architecture x86_64 --boot-mode uefi --tpm-support v2.0 \
  --ena-support --virtualization-type hvm \
  --root-device-name /dev/xvda \
  --block-device-mappings "DeviceName=/dev/xvda,Ebs={SnapshotId=<snap>,VolumeType=gp3,DeleteOnTermination=true}"
```

`--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](/docs/self-hosting/tutorial-deploy-aws).

<Warning>
  `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).
</Warning>
