From f9bd586265ac0c3c1572c39df68ebb14a16834b6 Mon Sep 17 00:00:00 2001 From: Julian Cuni Date: Tue, 23 Jun 2026 19:29:36 +0200 Subject: [PATCH] =?UTF-8?q?docs(wiki):=20session=20context=20=E2=80=94=20f?= =?UTF-8?q?irst=20booth=20go-live=20(user=20split,=20Docker=20deploy,=20we?= =?UTF-8?q?b=20access)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit appliance-provisioning.md: new §5c (admin/operator OS user split — verified; strip lxd/lpadmin/docker from the operator) + fleshed-out §6 runtime (resolute codename caveat, the standalone deploy dir + .env, the deploy commands, seed-admin, healthy-startup signal, and the web-access gotchas). log.md: the [2026-06-23] go-live entry (CI uv fix, compose env passthrough, relative /api, Caddy proxy). Container-deployment "Web access" section already landed last commit. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V --- wiki/decisions/appliance-provisioning.md | 69 +++++++++++++++++++++--- wiki/log.md | 24 +++++++++ 2 files changed, 85 insertions(+), 8 deletions(-) diff --git a/wiki/decisions/appliance-provisioning.md b/wiki/decisions/appliance-provisioning.md index 288b4fc..794ef0a 100644 --- a/wiki/decisions/appliance-provisioning.md +++ b/wiki/decisions/appliance-provisioning.md @@ -148,20 +148,73 @@ pressing `e` at the menu prompts for `admin` + password. Store the GRUB password > OS hardening on the first unit is now COMPLETE: LUKS FDE + TPM auto-unlock (PCR 7) + Secure Boot > (Deployed) + GRUB edit-lock. +## 5c. OS user model — admin vs operator (VERIFIED 2026-06-23) + +The OS has TWO roles and they must be different identities ([[threat-model]]: the operator is the +adversary). Create a dedicated **admin** (real password, sudo, NO auto-login) and keep the +**operator** as an auto-login, UNPRIVILEGED account. + +```bash +sudo adduser admin && sudo usermod -aG sudo admin +# VERIFY in a second session: log in as admin → `sudo whoami` prints root — BEFORE the next step: +sudo deluser sudo # demote the auto-login operator +groups # confirm: no 'sudo' +``` + +⚠ Order matters: confirm the new admin's sudo works **before** demoting the operator, or you lock +yourself out. Keep auto-login on the OPERATOR, not admin. **Leave root password disabled** (Ubuntu +default) — `admin`+sudo IS the root path; enabling root adds risk, no gain. + +> Strip latent escalation groups from the operator: **`sudo deluser lxd`** (lxd group = +> launch a privileged container that mounts host `/` as root — undoes the no-sudo hardening) and +> `lpadmin` (printer admin, unneeded). And NEVER add the operator to `docker` (also root-equivalent). + ## 5b. Further hardening (TODO — not yet done) - **Key-based SSH only** (disable password auth) if SSH is enabled at all. -- **No/locked-down desktop** — single-purpose; autostart the kiosk ([[desktop-shell-tauri]]). +- **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 (`linux-*-7.0.0-14`) once the new one is proven. +- `sudo apt autoremove` the leftover old kernel once the new one is proven. -## 6. Runtime — Docker stack +## 6. Runtime — Docker stack (VERIFIED 2026-06-23) -Per [[container-deployment]]: install Docker Engine + compose, then run the `parking-server` + -`parking-vision` images via `docker-compose.yml -f docker-compose.prod.yml`. Provide a real -`JWT_SECRET` (`openssl rand -hex 32`) and `COOKIE_SECURE=0` (plain-http booth LAN — see -[[disk-os-hardening]] deploy-time runbook). Images are published to the Gitea registry by -`build-images.yml` on push to dev/main. +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): + +``` +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. +``` + +```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 +``` + +Healthy startup 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. ## Quick-reference: the gotchas, in order they bit us diff --git a/wiki/log.md b/wiki/log.md index a3742d2..d8d28d5 100644 --- a/wiki/log.md +++ b/wiki/log.md @@ -1516,3 +1516,27 @@ prompts for admin+password. OS hardening on unit 1 is now COMPLETE: LUKS FDE + T step, §5b further-hardening TODO: SSH key-only, kiosk lockdown, signing key→TPM, autoremove old kernel) + [[disk-os-hardening]]. STILL TODO on the box: Docker install + run the parking stack (needs the images pushed — dev push + registry secrets pending). + +## [2026-06-23] deploy | First booth GO-LIVE — Docker stack running + web-access fixes (CI uv, compose env, relative /api, Caddy) +Deployed the two images onto the hardened booth (Dell 7070, Ubuntu 26.04) and worked through the +real-world bring-up issues. (1) Operator/admin OS user split: created a dedicated sudo `admin` user, +removed the auto-login operator from `sudo` (and should drop `lxd`/`lpadmin` — lxd is a root-escape +path); admin is the only sudo, operator auto-logs in unprivileged. (2) Docker 29.6 installed; deploy +dir /opt/parking_solution with hand-copied compose + .env; registry login to git.infra.msai.al; the +stack came up clean — vision fast_alpr loaded from the BAKED cache (0 downloads → offline-first +confirmed on real hardware), server migrated /data, both healthy. (3) Seeded the first admin via +`docker compose exec server node scripts/seed-admin.mjs` (bcrypt, writes users table — NOT the signed +ledger). FIXES committed this session: CI `astral-sh/setup-uv` action failed on the Gitea runner → +install uv via its official curl script instead (both ci.yml + build-images.yml) [0a22eab]; the base +compose only forwarded JWT_SECRET/DATABASE_URL/VISION_URL → added COOKIE_SECURE (CRITICAL on plain- +http or login cookies never send), WS_ALLOWED_ORIGINS, EVENT_SIGNING_KEY, VISION_ENABLED [1092316]; +the SPA had VITE_API_BASE=http://127.0.0.1:3000 baked in (leaked from apps/web/.env.production, which +is for the TAURI build but Vite auto-loads it for every build) → server Dockerfile now empties it via +.env.production.local so the SPA uses RELATIVE /api and works from ANY host [77b2acb]; added a CADDY +reverse proxy (prod override) so the booth is reached on a clean port-80 URL, server goes internal, +Caddyfile binds :80 to match any hostname incl. parksystems.msai.al [c637b27]. NET RESULT: no domain +baked into any image — naming controlled by hosts/DNS on-site; admin can reach it from another LAN PC. +Verified the relative-/api + Caddy fix end-to-end locally (Host: parksystems.msai.al through :80 → +SPA + /api/auth/login reach the server, no CORS). See [[container-deployment]] "Web access", +[[appliance-provisioning]]. REMAINING on the box: push dev so CI rebuilds parking-server:dev with the +relative-/api fix, then pull on the booth; kiosk autostart; operator user lxd/lpadmin cleanup.