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:
@@ -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
|
||||
|
||||
|
||||
+24
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user