From 7eadf71a0bba113291407b6d99565891b0d24fc5 Mon Sep 17 00:00:00 2001 From: Julian Cuni Date: Sat, 27 Jun 2026 12:30:08 +0200 Subject: [PATCH] =?UTF-8?q?docs(wiki):=20appliance-provisioning=20?= =?UTF-8?q?=E2=80=94=20Komodo=20deploy=20is=20now=20the=20primary=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §6 split: §6 = Docker engine only; new §7 = the Komodo Periphery deploy (PRIMARY, verified end-to-end on park-buzi 2026-06-27): - 7a install Periphery (onboarding key, user-mode/outbound, runs as admin, no inbound port; core_address = Core's proxy URL) - 7b deploy the Stack in Core (registry+git accounts, per-booth [[..]] secrets, env incl. COOKIE_SECURE=0; seed admin via Komodo's container terminal — no SSH) - 7b-bis fleet-as-code via komodo/resources.toml + ResourceSync (empty diff = in sync) - 7c break-glass: manual booth.sh when mesh/Core is down Added Komodo deploy gotchas 7-11 (core_address is the proxy URL not :9120; git-auth ≠ registry-auth; user-mode vs /etc/komodo root_directory; core_address singular; empty-diff/disabled-Execute = success). §5b SSH TODO reframed (Komodo removes SSH from routine ops). Header + date updated; log entry added. Fixed a stale [[atecc608-secure-element]] alias in the prior log entry. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V --- wiki/decisions/appliance-provisioning.md | 163 +++++++++++++++++++---- wiki/log.md | 19 ++- 2 files changed, 153 insertions(+), 29 deletions(-) diff --git a/wiki/decisions/appliance-provisioning.md b/wiki/decisions/appliance-provisioning.md index 794ef0a..6904140 100644 --- a/wiki/decisions/appliance-provisioning.md +++ b/wiki/decisions/appliance-provisioning.md @@ -2,17 +2,18 @@ type: reference tags: [parking, deployment, appliance, hardening, runbook, offline-first] sources: [] -updated: 2026-06-23 +updated: 2026-06-27 status: settled --- # Appliance provisioning runbook (booth PC) Step-by-step to take a booth PC from factory Windows to a hardened, encrypted, container-running -parking appliance. Written from the **first real provisioning, 2026-06-23**, on the actual hardware -below — every command here was run and verified on that machine, including the firmware-specific -workaround. Companion to [[disk-os-hardening]] (the *why*), [[tpm]] (TPM analysis), and -[[container-deployment]] (the images this runs). +parking appliance. Written from the **first real provisioning, 2026-06-23** (hardening) + +**first Komodo deploy, 2026-06-27** (the runtime) — every command here was run and verified on the +actual hardware, including the firmware-specific workaround. Companion to [[disk-os-hardening]] (the +*why*), [[tpm]] (TPM analysis), [[container-deployment]] (the images), and +[[fleet-deployment-komodo]] (the deploy control plane this runbook's §7 uses). > ⚠ This box is the [[threat-model|outsider-with-the-box]] defence. The load-bearing anti-fraud > control is still [[reconciliation]] over the [[append-only-event-chain|signed chain]] — disk @@ -171,50 +172,140 @@ default) — `admin`+sudo IS the root path; enabling root adds risk, no gain. ## 5b. Further hardening (TODO — not yet done) -- **Key-based SSH only** (disable password auth) if SSH is enabled at all. +- **Key-based SSH only** (disable password auth) if SSH is enabled at all. Routine ops no longer + need SSH — Komodo Periphery (§7) drives deploys + gives a container terminal over the mesh — so + SSH can be locked down hard or disabled, leaving the mesh + Komodo as the management path. - **No/locked-down desktop + kiosk autostart** — single-purpose; the operator never reaches a shell ([[desktop-shell-tauri]]). - Consider moving the host **event-signing key into the TPM** (non-extractable) — [[tpm]], [[open-questions]] #12. - `sudo apt autoremove` the leftover old kernel once the new one is proven. -## 6. Runtime — Docker stack (VERIFIED 2026-06-23) +## 6. Runtime — Docker engine (VERIFIED 2026-06-23) Install Docker Engine + compose (as `admin`). NB Ubuntu 26.04 codename is **`resolute`**, which download.docker.com may not yet publish — pin the repo line to `noble`, OR use Ubuntu's `docker.io`. Add only `admin` to the `docker` group (root-equivalent — NEVER the operator). -Deploy from a standalone dir (hand-copied; no repo on the appliance), e.g. `/opt/parking_solution`: -`docker-compose.yml` + `docker-compose.prod.yml` (the Caddy/prod override) + `Caddyfile` + a `.env` -(chmod 600). The `.env` (driven into the containers by the base compose): +This gives the appliance the engine. **How the stack gets ONTO it is step 7** — and as of +2026-06-27 the primary path is **Komodo (remote, no-SSH)**, not a hand-copied dir. The manual +`docker compose` flow survives as a **break-glass fallback** (§7c). -``` -JWT_SECRET= # server REFUSES to boot without (>=32, no insecure default) -EVENT_SIGNING_KEY= -COOKIE_SECURE=0 # CRITICAL on plain-http or the auth cookie never sends → no login -WS_ALLOWED_ORIGINS=http:// # any REMOTE origin admins use (same-origin always passes) -VISION_ENABLED=1 -# REGISTRY/TAG default to git.infra.msai.al/mca/parking_solution + dev; set TAG=main to pin. -``` +## 7. Deploy the stack — Komodo Periphery (PRIMARY, 2026-06-27) + +The booth is driven by a central **Komodo Core** over the **NetBird** mesh. The appliance runs a +small **Periphery** agent that *dials out* to Core; Core then deploys the same compose files. No +inbound port on the booth, no SSH for routine ops. Full rationale + threat model: +[[fleet-deployment-komodo]]. Verified end-to-end on the first booth (`park-buzi`) 2026-06-27. + +### 7a. Install Periphery (on the booth, as `admin`) + +Prereq: the booth is on the **NetBird** mesh and can reach Core's reverse-proxy URL +(`https://komodo.infra.msai.al`). + +1. In Core: **Settings → Onboarding → + New Onboarding Key** (Name = the booth, e.g. `park-buzi`; + Expiry ~1 day; Pre-Existing Key empty). Copy the one-time `O-…` key. **Single-use** — delete it + after the agent connects. +2. On the booth, install Periphery in **user mode** (runs as `admin`, who is in `docker`; NO root + daemon; **outbound** → opens no inbound port): ```bash -docker login git.infra.msai.al # a read-only package token, not the account password -docker compose -f docker-compose.yml -f docker-compose.prod.yml config # dry-run: verify the merged env -docker compose -f docker-compose.yml -f docker-compose.prod.yml pull -docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d -# Seed the FIRST admin (DB starts empty → nobody can log in until this runs; idempotent): -docker compose -f docker-compose.yml -f docker-compose.prod.yml exec \ - -e ADMIN_USER=admin -e ADMIN_PASS='' server node scripts/seed-admin.mjs +curl -sSL https://raw.githubusercontent.com/moghtech/komodo/main/scripts/setup-periphery.py | python3 - --user \ + --core-address="https://komodo.infra.msai.al" \ + --connect-as="park-buzi" \ + --onboarding-key="O-…" +sudo loginctl enable-linger admin # so the user service starts at boot without a login ``` -Healthy startup logs: vision `Initialized LicensePlateDetector …` with NO "Downloading" (baked -weights), server `[migrate] done` → `SPA static serving enabled` → `Server listening`. The transient +- `--connect-as` is the **Server name in Core** — unique, stable, site-meaningful (the fleet's + primary key). Booth #2 = a different name (e.g. `park-durres`); never reuse one. +- `--core-address` is Core's **reverse-proxy URL** (the URL you load the Core UI at over the mesh), + NOT `:9120` — Core's container port `9120` is exposed-not-published; the agent reaches it through + the proxy. (Gotcha #7 below.) +- Config lands at `~/.config/komodo/periphery.config.toml`. The key field is **`core_address`** + (singular); `root_directory` must be a path `admin` can write (user-mode default is fine — a + `/etc/komodo` default from a system install would `Permission denied` for the user service). + +Verify: `systemctl --user status periphery` → active; the server **`park-buzi`** appears and goes +**OK/green** in Core → Servers. Then **delete the onboarding key**. + +### 7b. Deploy the Stack (in Core — by hand once, then code) + +Add **Registry Account** + **Git Account** for `git.infra.msai.al` (user `komodo`, tokens) in Core +so Periphery can clone the repo AND pull the private images. Two distinct credential types — the +git clone working does NOT imply the image pull is authed (gotcha #8). Per-booth secrets +(`park__jwt_secret`, `park__event_signing_key` — distinct values, `openssl rand -hex +32`) live in Core's **Variables/Secrets** store, referenced from the Stack as `[[…]]`. + +Create a **Stack** (UI → Stacks → New), name = the booth (`park-buzi`): + +- **Server:** `park-buzi` · **Source:** repo `mca/parking_solution`, branch `dev`, files + `docker-compose.yml` + `docker-compose.prod.yml` · **Registry account:** `komodo` (else the pull + is anonymous → `no basic auth credentials`). +- **Environment** (Komodo writes this to a `.env` on the booth at deploy, substituting `[[…]]`): + +``` +REGISTRY=git.infra.msai.al/mca/parking_solution +TAG=dev # moving tag (staging). PIN to dev- for a live booth. +COOKIE_SECURE=0 # CRITICAL on plain-http or the auth cookie never sends → no login +VISION_ENABLED=1 +WS_ALLOWED_ORIGINS= # browser at the booth URL is same-origin; leave empty (the + # Tauri desktop app needs its origin here — separate task) +JWT_SECRET=[[park_buzi_jwt_secret]] +EVENT_SIGNING_KEY=[[park_buzi_event_signing_key]] +``` + +Deploy → Periphery pulls + `compose up`s. All containers (`proxy`/Caddy, `server`, `vision`) green. +Seed the FIRST admin (DB starts empty → nobody can log in until this runs; idempotent) **via +Komodo's terminal on the `server` container** (no SSH): + +```bash +docker exec -it -e ADMIN_USER=admin -e ADMIN_PASS='' \ + park-buzi-server-1 node scripts/seed-admin.mjs +``` + +> **Secrets-on-disk note.** The generated `.env` lands on the booth with **cleartext** secrets +> (compose needs real values). That's why the disk is LUKS-encrypted (§3–4) and keys are per-booth +> — the encryption is the control, and a single-booth compromise leaks only that booth's key. See +> [[fleet-deployment-komodo]] (the `EVENT_SIGNING_KEY`-in-Core blast-radius caveat; ATECC608 is the +> intended long-term signer). + +### 7b-bis. Fleet-as-code (`resources.toml`) — optional but recommended + +The repo's `komodo/resources.toml` mirrors the working Stack. Pointing a Core **ResourceSync** at +it makes the fleet **git-managed**: booth #N is a copy-pasted `[[stack]]` block; an image bump is a +one-line `TAG=` edit + push + Execute; every change is an auditable commit; a rebuilt Core +re-creates everything from the file. Keep the sync **Unmanaged** + **Delete-Unmatched OFF** until +trusted. An **empty diff / disabled Execute = the file already matches the live Stack** (success, +not an error). See `komodo/README.md` and [[fleet-deployment-komodo]]. + +### 7c. Break-glass — manual compose (mesh/Core down) + +When the mesh or Core is unreachable, the same compose files run locally via `scripts/booth.sh` +(or raw `docker compose`). Needs a local `.env` and a `docker login git.infra.msai.al` (a +read-only package token). This is the FALLBACK, not the routine path: + +```bash +docker login git.infra.msai.al +ENV=prod ./booth.sh config # dry-run the merged env +ENV=prod ./booth.sh up +``` + +`booth.sh` runs from wherever it sits next to the compose files (the booth deploys them flat, e.g. +`/opt/parking_systems/`). See [[container-deployment]]. + +### Healthy startup + web-access + +Healthy logs: vision `Initialized LicensePlateDetector …` with NO "Downloading" (baked weights), +server `[migrate] done` → `SPA static serving enabled` → `Server listening`. The transient `vision-service -> offline` at boot then `-> ready (fast_alpr)` ~8s later is normal (monitor polls before vision finishes loading). Reach the UI at **`http:///`** (Caddy on :80). **Web-access gotchas (all fixed in the images/compose — see [[container-deployment]] "Web access"):** the SPA uses a RELATIVE `/api` base (works from any host; do NOT bake a domain) + a Caddy proxy gives the clean port-80 URL; the domain (`parksystems.msai.al`) is pointed at the booth's LAN IP via -`hosts`/DNS ON-SITE, never an image rebuild. +`hosts`/DNS ON-SITE, never an image rebuild. The **Tauri desktop app** is hardcoded to +`localhost:3000` (CSP + endpoints) and can't reach a remote booth without code changes — a browser +works; the desktop app is a separate workstream. ## Quick-reference: the gotchas, in order they bit us @@ -225,3 +316,19 @@ the clean port-80 URL; the domain (`parksystems.msai.al`) is pointed at the boot 5. Always keep the **password slot** + an off-machine copy of the passphrase (TPM is never the only key). 6. GRUB password MUST be **edit-only** (`--unrestricted` on entries) or it prompts on EVERY boot → breaks unattended reboot. Verify `grep -c unrestricted /boot/grub/grub.cfg` ≥1 before rebooting. + +### Komodo deploy gotchas (2026-06-27) + +7. Periphery `core_address` is **Core's reverse-proxy URL** (`https://komodo.infra.msai.al`), NOT + `100.x:9120`. Core's `9120` is exposed-not-published (`docker ps` shows `9120/tcp` with no `->`) + → a direct dial gets `Connection refused`. Ping/SSH working over the mesh does NOT mean `:9120` + is reachable. +8. **Git auth ≠ registry auth.** The repo cloning fine does not mean image pull is authed — they're + separate Komodo credentials. A blank registry account on the Stack → anonymous pull → + `no basic auth credentials`. Set the Stack's **Registry Account** (`komodo`). +9. **User-mode Periphery + `/etc/komodo` `root_directory` = `Permission denied`** writing the agent + key. User-mode (runs as `admin`, no root daemon) must keep `root_directory` under `$HOME`. +10. The config key is **`core_address`** (singular). And `--core-address` derives `wss://` from + `https://` — if Core were plain-HTTP you'd need `http://` (→ `ws://`). +11. ResourceSync **Execute disabled + file shown clean in Info = empty diff = already in sync** + (success). Execute only enables when the file and Core diverge (e.g. you edit `TAG`). diff --git a/wiki/log.md b/wiki/log.md index 1bf5953..6346fa0 100644 --- a/wiki/log.md +++ b/wiki/log.md @@ -1695,8 +1695,25 @@ break-glass. Three settled choices: many/growing fleet · deploys **manual + pin `dev-` (no webhook — preserves the determinism we chose by pinning) · secrets **Komodo-managed, per-booth + unique**. Threat-model caveats recorded: Periphery is a root agent (bind to NetBird interface only, passkey+TLS, part of the TCB); `EVENT_SIGNING_KEY` in Core is a -fraud-root blast radius → per-booth keys + [[atecc608-secure-element|ATECC608]] as the real +fraud-root blast radius → per-booth keys + [[atecc608|ATECC608]] as the real long-term signer; Core becomes Tier-0. GPL-3.0 OK (external ops tooling, not a shipped dep — same boundary logic as the AGPL vision exception). New page [[fleet-deployment-komodo]]; infra-as-code sketch in `komodo/` (`resources.toml` + README + `.env.komodo.example`). Catalogued in `index.md`; `container-deployment` cross-linked + reframed (booth.sh = fallback). + +## [2026-06-27] deploy | First Komodo booth deploy VERIFIED end-to-end (park-buzi) + +Took the first booth through the whole Komodo flow on real hardware (Core v2.1.2 → agent reported +v2.2): onboarding key → Periphery installed **user-mode** (runs as `admin`, no root daemon, +**outbound** so the booth opens no inbound port) → server `park-buzi` **OK** in Core → Stack +(repo `mca/parking_solution`@`dev`, base+prod compose, registry account `komodo`, per-booth +`[[…]]` secrets) → all containers green → admin seeded via Komodo's container terminal (no SSH). +Then `komodo/resources.toml` rewritten to mirror the **working** Stack (exported from Core, v2.2 +field shape, **Stack-only — no `[[server]]`** since onboarding owns the server), committed + pushed +(`dev` 9918f27); a ResourceSync reads it clean — **empty diff / Execute disabled = already in +sync** (success, not error). `booth.sh` fixed for the flat `/opt` layout earlier (83298bc). +Gotchas that bit us (now in [[appliance-provisioning]] §7 + gotchas 7–11): `core_address` is Core's +**proxy URL** not `:9120` (exposed-not-published → Connection refused); **git-auth ≠ registry-auth** +(blank registry account → `no basic auth credentials`); user-mode + `/etc/komodo` root_directory → +`Permission denied`; config key is **`core_address`** singular. [[appliance-provisioning]] §6 split: +§6 = engine, §7 = Komodo deploy (PRIMARY) with §7c manual `booth.sh` break-glass.