From 9918f278b2f69102eae10069568a1fdf8b965ba4 Mon Sep 17 00:00:00 2001 From: Julian Cuni Date: Sat, 27 Jun 2026 12:14:04 +0200 Subject: [PATCH] =?UTF-8?q?feat(deploy):=20Komodo=20fleet=20deployment=20?= =?UTF-8?q?=E2=80=94=20resources.toml=20+=20decision?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adopt Komodo Periphery (over the NetBird mesh) as the booth fleet control plane, superseding SSH-and-booth.sh. The booth runs the SAME compose files; Komodo Core drives them remotely. booth.sh is demoted to a break-glass local fallback. - komodo/resources.toml mirrors the working park-buzi Stack (built by hand in the Core UI, then exported to TOML — field names match the running v2.2). Stack-only: servers are created by the agent onboarding OUTBOUND (one-time onboarding key → Periphery self-registers, auto-rotating keys, booth opens no inbound port), so there is no [[server]] block. Per-booth secrets via [[...]] refs to Core's store. - komodo/README.md + .env.komodo.example document the flow and the hard rules (no webhook; onboarding/outbound/mesh-only; per-booth unique secrets; never down -v the ledger volume). - wiki/decisions/fleet-deployment-komodo.md records the decision + threat-model analysis (Periphery is a root agent → mesh-bound; EVENT_SIGNING_KEY-in-Core is a fraud-root blast radius until ATECC608 signs; Core is now Tier-0; GPL-3.0 is fine as external ops tooling). container-deployment reframed (booth.sh = fallback); index + log updated. Verified end-to-end against a real booth (park-buzi): onboarded OK, Stack deployed, all containers green, admin seeded. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V --- komodo/.env.komodo.example | 24 ++++ komodo/README.md | 60 +++++++++ komodo/resources.toml | 51 ++++++++ wiki/decisions/container-deployment.md | 6 + wiki/decisions/fleet-deployment-komodo.md | 151 ++++++++++++++++++++++ wiki/index.md | 1 + wiki/log.md | 28 ++++ 7 files changed, 321 insertions(+) create mode 100644 komodo/.env.komodo.example create mode 100644 komodo/README.md create mode 100644 komodo/resources.toml create mode 100644 wiki/decisions/fleet-deployment-komodo.md diff --git a/komodo/.env.komodo.example b/komodo/.env.komodo.example new file mode 100644 index 0000000..c4951e0 --- /dev/null +++ b/komodo/.env.komodo.example @@ -0,0 +1,24 @@ +# Komodo Stack environment — reference of what a booth Stack needs and WHERE it comes +# from. Under Komodo, plain env lives in the Stack definition (komodo/resources.toml); +# the two SECRETS come from Komodo Core's secret store, PER BOOTH and UNIQUE. This file +# is documentation only — do NOT fill in real secrets here (it would be the same leak +# we're avoiding). See wiki/decisions/fleet-deployment-komodo.md. + +# --- plain Stack env (lives in resources.toml; safe in git) ------------------- +REGISTRY=git.infra.msai.al/mca/parking_solution +# IMMUTABLE per-commit tag. Manual + pinned. Bump per deploy. Never the moving `dev` +# on a production booth. +TAG=dev-830993b +COOKIE_SECURE=0 +VISION_ENABLED=1 +WS_ALLOWED_ORIGINS= + +# --- secrets (Core secret store, referenced by name in resources.toml) -------- +# Generated PER BOOTH (openssl rand -hex 32), registered in Core under booth-scoped +# names, never reused across sites. The user generates these; they are never typed on +# a CLI or committed. +# JWT_SECRET -> [[booth__jwt_secret]] (login) +# EVENT_SIGNING_KEY -> [[booth__event_signing_key]] (ledger signing — fraud root) +# Periphery/registry auth also live in Core: +# periphery passkey -> [[periphery_passkey_booth_]] +# registry account -> [[gitea_registry_account]] diff --git a/komodo/README.md b/komodo/README.md new file mode 100644 index 0000000..298d641 --- /dev/null +++ b/komodo/README.md @@ -0,0 +1,60 @@ +# `komodo/` — fleet deployment as code + +Infra-as-code for the **Komodo Core** control plane that deploys the parking appliance to the +booth fleet over the **NetBird** mesh. See `wiki/decisions/fleet-deployment-komodo.md` for the +rationale, threat-model analysis, and the three settled choices (many/growing fleet · deploys +are **manual + pinned** · secrets are **Komodo-managed, per-booth**). + +This directory does **not** change how images are built or how the app runs — it's only the +control plane. The booth still runs the same `docker-compose.yml` + `docker-compose.prod.yml` +([[container-deployment]]); Komodo just drives them remotely instead of someone SSH-ing in to +run `booth.sh`. + +## Files + +- **`resources.toml`** — the Komodo resource definitions (Servers, Stacks, optional Builders/ + Procedures), synced into Core via a **ResourceSync**. This is the reviewable, version- + controlled source of truth for *which booth runs what*. +- **`.env.komodo.example`** — the variables a Stack expects, documenting what comes from Core's + **secret store** (per-booth `JWT_SECRET` / `EVENT_SIGNING_KEY`) vs. plain Stack env. + +## How Core consumes this (one-time) + +In Komodo Core, create a **ResourceSync** pointing at this repo + path (`komodo/resources.toml`), +on the branch you manage from (e.g. `main`). Core reads the file and reconciles Servers/Stacks to +match. Thereafter, a PR to this directory + a sync is how you change the fleet — no clicking. + +> Komodo's TOML schema evolves across releases. Treat `resources.toml` as a **starting sketch**: +> `resources.toml` mirrors the **working `park-buzi` Stack** (built by hand in the Core UI, then +> exported to TOML — so field names match the running Komodo version, v2.2). Import it into the +> sync **Unmanaged** first and review the diff; it should be ~empty against the live Stack. + +## How servers get created — NOT here + +There is **no `[[server]]` block** in `resources.toml`. Servers are created by the **Periphery +agent onboarding outbound**: in Core, create a one-time **Onboarding Key** (Settings → +Onboarding), then install Periphery on the booth passing `--onboarding-key` + `--core-address` +(Core's reverse-proxy URL, reached over the NetBird mesh) + `--connect-as=`. The +agent self-registers, generates its own auto-rotating key pair (private key never leaves the +booth), and connects **outbound** — the booth opens **no inbound port**. The sync owns only the +**Stack**, which references the server by the name it onboarded as (`server = "park-buzi"`). See +`wiki/decisions/fleet-deployment-komodo.md`. + +## Adding booth N + +Copy the `[[stack]]` block, change `name`, `server` (its onboarded name), and the per-booth +secret references (`[[park__jwt_secret]]`, `[[park__event_signing_key]]`). Create +those secrets in Core's store first. + +## Hard rules encoded here (do not relax without updating the decision page) + +1. **No deploy webhook on a booth Stack.** Deploys are a human action; pin `TAG=dev-` before + a production booth goes live. A moving `:dev` on a production booth is the non-determinism we + rejected. (`TAG=dev` here is fine while staging.) +2. **Onboarding, outbound, mesh-only.** Servers self-register via an onboarding key; Periphery + connects outbound to Core's mesh URL and exposes no inbound port. Never a LAN/WAN address. +3. **Secrets are per-booth and unique.** `EVENT_SIGNING_KEY` signs the anti-fraud ledger — one + leak must taint one booth, never the fleet. Reference Core secrets by name; never inline a + real value in this file (it's in git). +4. **Volumes preserved.** The Stack must never run `compose down -v` — that would wipe the + `parking-data` volume (the signed ledger). Komodo's "destroy" is gated for the same reason. diff --git a/komodo/resources.toml b/komodo/resources.toml new file mode 100644 index 0000000..0e08091 --- /dev/null +++ b/komodo/resources.toml @@ -0,0 +1,51 @@ +# Komodo resources — parking appliance fleet (control plane as code) +# +# Synced into Komodo Core via a ResourceSync pointing at this file. Drives the SAME +# compose files the booth runs locally (docker-compose.yml + docker-compose.prod.yml); +# Komodo Periphery on each booth executes them. See: +# wiki/decisions/fleet-deployment-komodo.md (rationale + threat model) +# wiki/decisions/container-deployment.md (image build/tag/registry — unchanged) +# +# This file mirrors the WORKING park-buzi Stack (built by hand in the Core UI, then +# exported to TOML). Field names match the running Komodo version (v2.2). +# +# NO [[server]] block: servers are created by the AGENT onboarding outbound (a one-time +# onboarding key → Periphery self-registers with auto-rotating key pairs). The sync owns +# only the Stack; it references the server by the name it onboarded as (`connect_as`). +# +# Secrets ([[park_buzi_jwt_secret]] etc.) are REFERENCES to Komodo Core's secret store — +# per-booth + unique, never inlined here (this file is in git). JWT_SECRET gates login; +# EVENT_SIGNING_KEY signs the append-only anti-fraud ledger. +# +# Deploys are MANUAL + PINNED in spirit: bump TAG to an immutable dev- before a +# production booth goes live (TAG=dev here is the moving tag, fine while staging). NO +# deploy webhook is attached to a booth Stack. + +############################################################################## +# Stack — the deployable unit for booth "park-buzi". One Stack per booth; add a +# new [[stack]] block per site (unique name, its own per-booth secret refs). +############################################################################## + +[[stack]] +name = "park-buzi" +[stack.config] +server = "park-buzi" +git_provider = "git.infra.msai.al" +git_account = "komodo" +repo = "mca/parking_solution" +branch = "dev" +file_paths = [ + "docker-compose.yml", + "docker-compose.prod.yml" +] +registry_provider = "git.infra.msai.al" +registry_account = "komodo" +environment = """ +REGISTRY=git.infra.msai.al/mca/parking_solution +TAG=dev +COOKIE_SECURE=0 +VISION_ENABLED=1 +WS_ALLOWED_ORIGINS= +JWT_SECRET=[[park_buzi_jwt_secret]] +EVENT_SIGNING_KEY=[[park_buzi_event_signing_key]] +""" diff --git a/wiki/decisions/container-deployment.md b/wiki/decisions/container-deployment.md index fca6ba3..93f90d5 100644 --- a/wiki/decisions/container-deployment.md +++ b/wiki/decisions/container-deployment.md @@ -12,6 +12,12 @@ How the parking system's runtime apps are packaged as containers, tagged, and pu Settled 2026-06-22. Companion to [[vision-service-packaging]] (which scopes the vision service into the monorepo) and the desktop [[desktop-shell-tauri]] (a separate, tag-only bundle). +> **The build/tag/registry pipeline below is current.** What changed (2026-06-27): the +> *deploy mechanism* is no longer "SSH in and run `booth.sh`". At fleet scale that's superseded +> by [[fleet-deployment-komodo]] (Komodo Periphery over a NetBird mesh, driving these same +> compose files). `scripts/booth.sh` is now a **break-glass local fallback**, not the primary +> deploy path. + ## Two images (the desktop app is NOT containerized) - **`parking-server`** — the Fastify API **plus the built React SPA**. One container serves both: diff --git a/wiki/decisions/fleet-deployment-komodo.md b/wiki/decisions/fleet-deployment-komodo.md new file mode 100644 index 0000000..33b3968 --- /dev/null +++ b/wiki/decisions/fleet-deployment-komodo.md @@ -0,0 +1,151 @@ +--- +type: decision +tags: [parking, deployment, fleet, komodo, netbird, offline-first, threat-model] +sources: [] +updated: 2026-06-27 +status: settled +--- + +# Fleet deployment — Komodo Periphery over a NetBird mesh + +How the parking appliance is deployed and managed **at fleet scale**, superseding the +single-box, SSH-and-`booth.sh` model. The image build/tag/registry pipeline +([[container-deployment]]) is unchanged — this decides only the *control plane* that drives +those same compose files onto many booths. Settled 2026-06-27. + +## The problem `booth.sh` couldn't solve + +[[container-deployment|`scripts/booth.sh`]] is a thin wrapper over `docker compose -f base -f +prod --env-file .env`. It works for **one** appliance you can get a shell on, but as the fleet +grows (the stated direction is **many/growing** sites) it gives us none of: + +- **Remote, no-SSH operation** — an update means someone gets a root shell on the booth. +- **A fleet view** — which booth runs which `dev-`, which is healthy/offline. +- **A deploy audit trail** — who deployed what, when. +- **One-click rollback** to a previous immutable `dev-`. + +These are exactly the gaps a deployment controller fills. We already run every prerequisite +(a **Komodo Core**, a **NetBird** zero-trust mesh, the **Gitea registry**), so the marginal +cost is low. + +## Decision + +Adopt **Komodo Periphery** on each appliance, driven by the existing **Komodo Core** over the +**NetBird** mesh. Keep the compose files and the [[container-deployment|image pipeline]] +verbatim — Komodo consumes them as a *Stack*; it does not replace them. `booth.sh` is demoted +to a **break-glass local fallback** for when the mesh/Core is unreachable. + +``` +Gitea push ─▶ build-images.yml ─▶ registry (parking-server:dev-, parking-vision:dev-) + │ +Komodo Core (off-site) ──── NetBird mesh ─┼─▶ Periphery @ booth-A ─▶ docker compose up (pinned sha) + • fleet table / history / rollback ├─▶ Periphery @ booth-B + • per-booth secret injection └─▶ Periphery @ booth-C … + • NO deploy webhook (manual + pinned) +``` + +### The three load-bearing choices (settled with the user 2026-06-27) + +1. **Fleet size: many/growing.** Komodo is treated as load-bearing infrastructure, not a + convenience. This is what tips the decision away from "SSH-over-NetBird + a playbook". +2. **Deploy trigger: always manual + pinned.** **No deploy webhook on a booth Stack.** A human + deploys a specific immutable `TAG=dev-` from Core. This preserves the determinism we + chose when pinning the booth tag (a moving `:dev` auto-redeploying a production booth is the + surprise we explicitly rejected). A *staging* booth MAY track `:dev`; a production booth + never does. +3. **Secrets: Komodo-managed (per-booth, unique).** Core's secret store injects `JWT_SECRET` + and `EVENT_SIGNING_KEY` into the Stack at deploy. This scales (no SSH-to-N-booths to rotate + a key) — but see the threat-model tension below; the keys MUST be **distinct per booth**. + +## Why this is safe (against the project's two forces) + +### Offline-first ([[offline-first]]) — Core is orchestration, never a runtime dependency + +The booth must run **fully when the mesh is down**. Komodo's agent model satisfies this: +Periphery + the local containers keep operating if Core is unreachable; we lose *remote +management* until the mesh returns, **not operation**. There must be **no runtime path** from +booth operation to Core — Core only deploys. (Periphery's own liveness is irrelevant to entry/ +exit; the Fastify server and SQLite ledger run independently of it.) + +### Threat model — the adversary is the booth operator ([[threat-model]]) + +This is the sharp edge, and the reason this page is explicit rather than a footnote. + +- **Periphery is a root-capable remote-exec agent on the appliance.** If the operator + compromises the box, the agent is a lever. Mitigations: bind Periphery **only to the NetBird + interface** (never `0.0.0.0`), enforce its **passkey + TLS**, and fold the agent into the + [[disk-os-hardening]] surface. It is part of the trusted computing base now. +- **`EVENT_SIGNING_KEY` is the anti-fraud root.** It signs the [[append-only-event-chain| + append-only ledger]] — the control between us and a booth operator forging entry/exit events. + Holding it in Core means **a Core compromise can forge any booth's ledger that shares a key**. + Two mitigations make central management acceptable: + - **Per-booth, unique keys.** Never reuse a signing key across sites, so a single leak taints + one booth, not the fleet. + - **The [[atecc608|ATECC608]] is the real long-term signer.** The + `EVENT_SIGNING_KEY` HMAC is the *interim* mechanism; once the secure element signs the + chain, the key in Core stops being the fraud root. Tracked in [[open-questions]]. +- **Core becomes a Tier-0 asset.** It now holds login + ledger keys for the whole fleet, so it + must be hardened to the booths' bar: Komodo API bound to the NetBird mesh only, never a public + interface; access-controlled; backed up. + +### Licensing — Komodo is GPL-3.0, and that's fine here + +The hard MIT/Apache/BSD constraint ([[technology-stack]]) is about **shipped app dependencies** +(code we distribute/link). Komodo is **external ops tooling we self-host and don't distribute**, +so its GPL-3.0 does not taint the product — exactly like the [[vision-service|AGPL ANPR +exception]] reasoning (a separate process / external boundary, not a linked dependency). Noted +here so it isn't re-litigated. + +## What lives where + +| Concern | Where | Notes | +| --- | --- | --- | +| Image build + tags | Gitea CI ([[container-deployment]]) | unchanged: `:dev` moving + `:dev-` immutable | +| Compose files | the repo + on the booth | unchanged base + `docker-compose.prod.yml` | +| Stack / deploy definition | **Komodo Core** | git-synced from `komodo/` (infra-as-code) | +| Which sha is deployed | **Komodo Core**, manual | `TAG=dev-`, pinned, no webhook | +| `JWT_SECRET`, `EVENT_SIGNING_KEY` | **Komodo Core** secret store | **per-booth, unique** | +| `COOKIE_SECURE=0`, `TAG`, `REGISTRY` | Komodo Stack env | per-environment | +| Registry pull creds | **Komodo Core** | so Periphery can pull from Gitea | +| Local break-glass | `booth.sh` + a local `.env` | mesh-down fallback only | + +## Setup outline + +**On each appliance** (after [[appliance-provisioning]]): +1. Install **Komodo Periphery** (binary or container), bound **only** to the NetBird interface; + set its passkey/TLS. +2. Point its compose/stack dir at `/opt/parking_systems/` (the existing files). +3. Keep `booth.sh` + a minimal local `.env` (no real secrets) as break-glass. + +**In Komodo Core:** +1. Add the booth as a **Server**, address = its **NetBird IP** (mesh, not LAN/WAN). +2. Define the **Stack** = base + `docker-compose.prod.yml`, env from Core's secret store, secrets + **per booth**. +3. **No deploy webhook** on the booth Stack — deploys are manual; set `TAG=dev-` explicitly. +4. Add Gitea registry creds so Periphery can pull. +5. Sync the Stack/Server definitions from the repo's `komodo/` directory (infra-as-code: + `komodo/resources.toml` + README) so the control plane is itself reviewable + + version-controlled. + +## Open / not yet done + +- **Per-booth secret generation + rotation flow** — how a new site's unique `EVENT_SIGNING_KEY` + is generated and registered in Core (vs. on-site `openssl rand`). Tie-in: [[open-questions]] + JWT-key item. +- **ATECC608 as the signer** supersedes `EVENT_SIGNING_KEY`-in-Core as the fraud root — until + then central secrets carry the blast-radius noted above. +- **Periphery hardening checklist** folded into [[disk-os-hardening]] (interface binding, passkey, + TLS, agent as TCB). +- **Staging vs production booth split** (a staging booth on `:dev` with a webhook; production + manual+pinned) — not yet modelled in `komodo/`. +- **Core backup / DR** — Core is now Tier-0; its loss = no fleet management (operation + unaffected, per offline-first). Backup story TBD. + +## Supersedes / relates + +- **Supersedes** the "SSH + `booth.sh` is the deploy mechanism" assumption in + [[container-deployment]] (that page's *build/tag/registry* content stands; its `booth.sh`-as- + primary-deploy framing is now the fallback). Cross-linked there. +- Companion: the `komodo/` infra-as-code sketch (in the repo, not the wiki), + [[appliance-provisioning]] (what runs *before* Periphery), [[disk-os-hardening]] (the + appliance's hardening surface). diff --git a/wiki/index.md b/wiki/index.md index 16daaa1..1fe77ab 100644 --- a/wiki/index.md +++ b/wiki/index.md @@ -125,4 +125,5 @@ Counts: 4 sources · 19 entities · 45 concepts · 7 decision records. - [[event-streams-split]] — split the signed business ledger (ledger_events) from unsigned device telemetry (device_events). - [[desktop-shell-tauri]] — ✅ Tauri v2 chosen over Electron for the desktop kiosk shell; thin wrapper, server keeps all logic. Best case Ubuntu 26.04 LTS (resolves WebKitGTK); worst case Windows+WSL → kiosk browser, no native shell. - [[container-deployment]] — Docker images for the non-desktop apps: parking-server (Fastify API + bundled SPA via @fastify/static) + parking-vision (Python/uv ANPR); branch+SHA tags, per-env compose, Gitea registry, build-images.yml CI; pnpm deploy (not prune) for native better-sqlite3; migrate-at-boot. +- [[fleet-deployment-komodo]] — fleet control plane: Komodo Periphery on each booth, driven by Komodo Core over a NetBird mesh, running the same compose files. Deploys manual + pinned to dev- (no webhook); secrets Komodo-managed per-booth+unique; booth.sh demoted to break-glass. Threat-model caveats: Periphery is a root agent (mesh-bound only), EVENT_SIGNING_KEY-in-Core is a fraud-root blast radius until ATECC608 signs. komodo/ is infra-as-code. - [[appliance-provisioning]] — booth-PC provisioning runbook (Dell 7070, i5-8500, discrete Nuvoton TPM): BIOS/Secure-Boot → direct-flash Ubuntu 26.04 USB (not Ventoy) → passphrase-LUKS install → manual PCR-7 TPM seal (workaround for the installer's dbt PCR_UNUSABLE error) → Docker. Verified on hardware 2026-06-23; TPM auto-unlock works. diff --git a/wiki/log.md b/wiki/log.md index 2464be4..1bf5953 100644 --- a/wiki/log.md +++ b/wiki/log.md @@ -1672,3 +1672,31 @@ unprivileged for the non-root container user. Verified on hardware: the readers ICMP on the device VLAN (eth1) — distinct MACs — and the UI Test connection shows "● ready — ping 10.0.10.7". (NB: an earlier "offline" reading was a WSL wrong-route artifact, not the readers.) Covered by `reader.test.ts` (4 tests). Documented in [[device-status-monitoring]]. Devices +4 tests, all green. + +## [2026-06-27] fix | booth.sh failed in the flat /opt layout (couldn't find compose files) + +The booth deploys the compose files **flat** in `/opt/parking_systems/` with `booth.sh` next to +them, but the script assumed `/scripts/` and did `cd ..` → `REPO_DIR=/opt` (no compose +files); `usage()` then `sed`-read a now-relative `$0` → "can't read booth.sh". That's why +`sudo ./booth.sh` only printed help and `/bin/bash booth.sh` errored. Fix: **discover** the +compose files (script's own dir → `../` → `$PWD`), `usage()` reads an absolute `$SELF`. Also: +`.env.example` defaulted `TAG=main`, but the registry only has `dev`/`dev-` (no main build) → +`compose pull` 404s; default to `TAG=dev` + documented the moving-vs-immutable tag scheme. +Reproduced the booth's flat layout in a scratch dir; all forms (`./booth.sh`, `/bin/bash +booth.sh`, `config`, absolute-path) verified. Commit 83298bc. + +## [2026-06-27] decision | Fleet deployment → Komodo Periphery over NetBird + +booth.sh hit its ceiling: fine for one SSH-able box, but no remote/no-SSH op, no fleet view, no +deploy history, no rollback — and the fleet is **many/growing**. Decision: **Komodo Periphery** +on each appliance, driven by an existing **Komodo Core** over the **NetBird** mesh, running the +**same** compose files ([[container-deployment]] pipeline unchanged); `booth.sh` demoted to +break-glass. Three settled choices: many/growing fleet · deploys **manual + pinned** to a +`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 +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).