docs(wiki): appliance-provisioning — Komodo deploy is now the primary flow
§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
This commit is contained in:
@@ -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=<openssl rand -hex 32> # server REFUSES to boot without (>=32, no insecure default)
|
||||
EVENT_SIGNING_KEY=<a DIFFERENT openssl rand -hex 32>
|
||||
COOKIE_SECURE=0 # CRITICAL on plain-http or the auth cookie never sends → no login
|
||||
WS_ALLOWED_ORIGINS=http://<name-or-ip> # 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='<strong-pw>' 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_<booth>_jwt_secret`, `park_<booth>_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-<sha> 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='<strong-pw>' \
|
||||
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://<name-or-ip>/`** (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`).
|
||||
|
||||
+18
-1
@@ -1695,8 +1695,25 @@ break-glass. Three settled choices: many/growing fleet · deploys **manual + pin
|
||||
`dev-<sha>` (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.
|
||||
|
||||
Reference in New Issue
Block a user