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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user