docs(wiki): session context — first booth go-live (user split, Docker deploy, web access)

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
This commit is contained in:
2026-06-23 19:29:36 +02:00
parent aa546235fb
commit f9bd586265
2 changed files with 85 additions and 8 deletions
+61 -8
View File
@@ -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 <operator> sudo # demote the auto-login operator
groups <operator> # 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 <operator> 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=<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.
```
```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
```
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://<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.
## Quick-reference: the gotchas, in order they bit us