Edge host preparation

Pre-install checklist for a building’s edge box, before you run infra/edge-bootstrap/install.sh.

Run through this BEFORE handing the box to the bootstrap script — the script assumes the host is freshly installed and pre-flighted, and will refuse to continue if it isn’t.

OS choice

Ubuntu Server 22.04 LTS. 24.04 also works (the bootstrap accepts both), but 22.04 is the safer default for the pilot:

  • It’s what infra/edge-bootstrap/Dockerfile.test builds against, so every commit’s CI run exercises it.
  • Longer track record with the fire-panel adapter stacks (bacpypes, pymodbus) — these libraries lag a release on new Ubuntu majors.
  • An extra year of LTS overlap with the panel vendor docs we’ve already cross-referenced.

If you really need 24.04 — say, the customer requires it for procurement reasons — it’s fine; just make sure to re-run the Tier-0 + Tier-1 perf tests at that point. PIL + numpy timings differ slightly between Python 3.10 (Jammy) and Python 3.12 (Noble) on the same hardware.

Not the Desktop edition. No GUI. The supervisor runs headless under systemd.

Hardware floor

The bootstrap script’s preflight enforces these — install will refuse on anything smaller.

SpecMinimumPilot box (Saudi Airlines Arafat)
Architecturex86_64x86_64
CPU8 coresXeon class or Ryzen 7
RAM32 GiB32–64 GiB
Disk256 GB SSD512 GB+ NVMe
NetworkWired Gigabit on the camera LANWired Gigabit
GPUOptionalOptional NVIDIA L4 if customer wants on-edge Tier 1

ARM (Raspberry Pi class) is explicitly rejected at preflight. There’s a separate Pi-class edge path in the plan but we haven’t built it yet — don’t try to fake it.

During Ubuntu install

  1. Encrypted full-disk install (LUKS). The encrypt-before-upload guarantee in the data-protection plan relies on the disk being encrypted at rest. Setting it at install time is dramatically easier than after — pick “Use LVM with the new Ubuntu Server installation” and tick the encryption box.

    Store the LUKS passphrase in a password manager. Not in the repo. Not in chat. It is the permanent break-glass recovery key.

    Unattended TPM auto-unlock is now standard (no more “person at the keyboard” on every reboot) — the bootstrap enrolls it via Clevis. The passphrase slot is never removed, so it always remains as the recovery path. The full story, the BIOS settings that complete the power-loss chain, and every gotcha are in Unattended encrypted boot (LUKS → TPM via Clevis) — read that one; it is the deep reference for this whole area.

  2. Static IP on the camera LAN. The fire panel + cameras are all LAN-routed. DHCP is fine if your network gear has reservations; static is better.

  3. Hostname matters. When we register the gateway in the cloud later, the hostname becomes part of the unique constraint per building (@@unique([buildingId, hostname]) in the Prisma schema). Pick something stable and meaningful:

    • edge-arafat-01 for the Saudi Airlines pilot
    • edge-<site>-<NN> as the general pattern
  4. User account: wiqaia (or whatever your team standard is) with sudo, no root login. The bootstrap script will create a separate non-login service account for the supervisor.

  5. SSH only, public-key login only. Start clean, but you don’t have to get this perfect by hand: the bootstrap’s harden_ssh step now fully disables password auth via a 00- sshd drop-in (sorts before cloud-init’s 50-) and pins cloud-init (ssh_pwauth: false) so a reboot can’t silently re-enable it. It validates with sshd -t and reloads (never restarts) so a live session is never dropped. Make sure your key works before install day — once password auth is off, the key is the only way in.

  6. No additional packages during install. Tick none of the “Featured Server Snaps” boxes. The bootstrap installs the exact pinned versions of Docker + Python + the supervisor binary — anything you install yourself before that risks version conflicts.

After first boot, before the bootstrap

# Confirm the box meets the floor:
nproc                    # ≥ 8
free -h                  # ≥ 32 GiB
df -h /                  # ≥ 256 GB
ip addr                  # camera-LAN interface UP, has the address you set
cat /etc/os-release      # PRETTY_NAME should say 22.04 or 24.04

# Confirm the disk is actually encrypted:
sudo cryptsetup status /dev/mapper/ubuntu--vg-root 2>/dev/null 
  || sudo cryptsetup status /dev/mapper/$(ls /dev/mapper | grep -v control | head -1)
# Look for "type: LUKS2" and "cipher: aes-xts-plain64"

# Unattended security updates only — the supervisor handles app updates:
sudo apt update && sudo apt -y upgrade
sudo apt -y install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

Firmware / BIOS (manual, one-time, per box — before it ships)

install.sh cannot set firmware portably, so do this in UEFI setup before the box leaves your hands. It is the first link in the power-loss self-heal chain — without it the box stays off after an outage and none of the unattended-unlock work matters.

  • Restore on AC Power Loss → Power On (not “Last State”, not “Off”).
  • Halt on errors → No / Continue (don’t pause boot waiting for F1).
  • Boot order → internal NVMe only.

Full rationale + the rest of the chain: Unattended encrypted boot.

Hand the box to the bootstrap script

Once the checks above pass, run infra/edge-bootstrap/install.sh. It is idempotent and self-logs to /var/log/wiqaia-edge-install.log. It will:

  1. Re-verify the OS + hardware floor (defense in depth).
  2. Install pinned Docker.
  3. Create the wiqaia-edge service account.
  4. Install + verify the supervisor binary.
  5. Install the systemd unit.
  6. Generate the gateway keypair.
  7. Harden SSH (password auth off + cloud-init pinned).
  8. Enroll LUKS → TPM (Clevis; unattended boot).
  9. Register the gateway with the cloud (one-time enrollment token).
  10. Start the supervisor.

Before the cloud exists (current pilot state) it’s a hybrid — steps 4 and 9 can’t reach the not-yet-deployed binary mirror / fleet manager, so:

  • --host-prep-only runs steps 1–8 and stops (re-run without it once cloud is up to do 9–10; idempotent).
  • --binary-file <path> installs a locally-built supervisor for step 4.
  • --skip-luks defers step 8; --luks-only runs only step 8 as a deliberate, separately-documented pass.

The LUKS step and its mandatory reboot proof have their own deep runbook — Unattended encrypted boot. Read it before doing step 8 on any box.

If any step fails, the script halts loud — read the message, fix the underlying issue, re-run. Successful steps no-op on re-run.

Things to write down somewhere durable

Keep these in your password manager (not the repo):

  • LUKS passphrase
  • The cloud-side enrollment token used in step 7 (it’s one-time-use, but you may want to know which token went to which box)
  • The supervisor’s logged gateway-id (printed once registration succeeds; matches the row in the cloud’s edge_gateway table)
  • The certificate fingerprint the supervisor generates (also logged; matches edge_gateway.certificate_fingerprint)

What the supervisor itself looks like

Being built incrementally (M09 work) — track Milestone 9 in Gitea for sequencing. The Go supervisor that watches camera-manager + tier0 + tier1 + the fire-panel adapter + the cloud control tunnel does build and run today: on the pilot box we built it from apps/edge-supervisor (go build ./cmd/supervisor, Go 1.22 auto-fetching the 1.25 toolchain from go.mod) and installed it via --binary-file. With no edge.yaml it correctly runs degraded (healthz only) — that’s expected pre-cloud, not a fault. Once the binary mirror is up, the bootstrap fetches released versions instead of a local build.

Common gotchas

  • “OS not supported” at preflight: usually means Desktop edition (the os-release PRETTY_NAME contains “Desktop”). Re-install with Server.
  • “insufficient cores”: VMs often default to 4. Bump the host config.
  • Bootstrap hangs on Docker pull: outbound to *.docker.io blocked. Customer network policy. We’re not married to Docker Hub — we can mirror via Artifact Registry, but that’s a config change in install.sh.
  • WireGuard tunnel fails to come up post-bootstrap: usually the customer’s firewall blocking UDP 51820 outbound. Confirm with the customer’s network team before install day.
WiqAIa+ Docs · Rizoma