diff --git a/wiki/concepts/disk-os-hardening.md b/wiki/concepts/disk-os-hardening.md index 87021d7..4c75c81 100644 --- a/wiki/concepts/disk-os-hardening.md +++ b/wiki/concepts/disk-os-hardening.md @@ -29,6 +29,14 @@ With LUKS in place, **SQLCipher becomes optional** defence-in-depth rather than layer. (The custom controller adds its own: ESP32 flash encryption + secure boot — see [[esp32-custom-controller]].) +> **Step-by-step OS install + TPM-seal procedure** (BIOS → encrypted install → manual PCR-7 TPM +> seal, with the Dell-7070-specific `dbt` workaround) lives in [[appliance-provisioning]] — written +> from the first real provisioning (2026-06-23) and verified on hardware. **OS hardening on the first +> unit is COMPLETE: LUKS FDE + TPM auto-unlock (PCR 7, unattended) + Secure Boot (Deployed) + GRUB +> edit-lock** (the GRUB password is the specific countermeasure to the `init=/bin/bash` root-shell +> hole that PCR-7 sealing does NOT cover). Resolves the implementation half of [[open-questions]] #12 +> for unit 1. + ## Deploy-time server configuration (runbook) Env in `apps/server/.env` on the appliance (see `apps/server/.env.example`). The security-load-bearing ones: diff --git a/wiki/decisions/appliance-provisioning.md b/wiki/decisions/appliance-provisioning.md new file mode 100644 index 0000000..288b4fc --- /dev/null +++ b/wiki/decisions/appliance-provisioning.md @@ -0,0 +1,174 @@ +--- +type: reference +tags: [parking, deployment, appliance, hardening, runbook, offline-first] +sources: [] +updated: 2026-06-23 +status: settled +--- + +# Appliance provisioning runbook (booth PC) + +Step-by-step to take a booth PC from factory Windows to a hardened, encrypted, container-running +parking appliance. Written from the **first real provisioning, 2026-06-23**, on the actual hardware +below — every command here was run and verified on that machine, including the firmware-specific +workaround. Companion to [[disk-os-hardening]] (the *why*), [[tpm]] (TPM analysis), and +[[container-deployment]] (the images this runs). + +> ⚠ This box is the [[threat-model|outsider-with-the-box]] defence. The load-bearing anti-fraud +> control is still [[reconciliation]] over the [[append-only-event-chain|signed chain]] — disk +> encryption + Secure Boot raise the cost of offline tamper, they don't replace reconciliation. + +## Reference hardware (first unit, 2026-06-23) + +- **Dell OptiPlex 7070**, **Intel Core i5-8500** (Coffee Lake), 238 GB SATA SSD (`/dev/sda`). +- **TPM 2.0 — discrete Nuvoton** (`Get-Tpm` → `ManufacturerIdTxt NTC`, fw 7.2.1.0). NOT Intel + PTT/fTPM. Discrete ⇒ an external LPC/SPI bus exists (bus-sniff is a theoretical physical attack on + PCR-only sealing — accepted; see [[tpm]]). Used PC — previous owner irrelevant. +- Shipped Windows 11; formatted to **Ubuntu 26.04 LTS** (the decided platform — [[desktop-shell-tauri]]). + +## 1. BIOS (F2 at the Dell logo) + +- **TPM**: leave **On**. Used PC → **Clear the TPM once** (Security → TPM → Clear) so the prior + owner's keys are wiped before LUKS enrollment. (PPI "Bypass for Clear" was unchecked → it asks for + physical confirmation at next boot; that's normal.) +- **Secure Boot**: **Enabled**, **Deployed Mode** (not Audit). **"Enable Custom Mode" UNCHECKED** = + Standard Mode with stock Microsoft keys — this is what Ubuntu's signed shim needs. Do NOT touch + PK/KEK/db/dbx. NB: the 7070's Expert Key Management is **edit-only** (Save/Replace/Append/Delete — + no read-only "View Key"), so you **cannot inspect db from BIOS**; verify via the live USB instead + (step 2). +- **Boot**: UEFI only (no CSM/Legacy — a Legacy install has no Secure Boot / TPM-seal path). +- Set a **BIOS admin password**. + +## 2. Boot the Ubuntu 26.04 USB (Secure Boot ON) + +- **Flash the ISO DIRECTLY** (Rufus GPT/UEFI, Etcher, or `dd`). **NOT Ventoy** — Ventoy's own + bootloader isn't in `db`, so Secure Boot rejects it with **`Verification failed: (0x1A) Security + Violation`** (this is Secure Boot working correctly, not a fault). A directly-flashed Ubuntu USB + boots the Microsoft-signed shim, which stock `db` trusts. +- **F12** at the Dell logo → pick the USB under **UEFI BOOT**. +- Reaching the installer with Secure Boot ON = positive proof the MS third-party UEFI CA is in `db` + (the verification the BIOS couldn't show us). + +## 3. Encrypted install — the firmware workaround (IMPORTANT) + +The 26.04 installer disk page offers: No Encryption / **Encrypt with a passphrase** / **Use +hardware-backed encryption** (+ advanced LVM/ZFS, both ZFS experimental). + +- **"Use hardware-backed encryption" FAILS on this 7070** with: + `PCR_UNUSABLE … error with secure boot policy (PCR7) measurements: generating secure boot profiles + for systems with timestamp revocation (dbt) support is currently not supported.` + → Ubuntu's *automated* FDE profiler can't model PCR7 on Dell firmware carrying a `dbt` (UEFI + timestamp revocation list). It is NOT a TPM or Secure-Boot fault — both are fine. +- **So: choose "Encrypt with a passphrase".** Set a strong passphrase and **SAVE IT OFF-MACHINE** + (phone / password manager). It is both the boot unlock (until TPM sealing) AND the permanent + recovery slot. Finish the install. +- Result (verify with `lsblk`): `sda1` vfat `/boot/efi`, `sda2` ext4 `/boot`, `sda3` `crypto_LUKS` + → `dm_crypt-0` (LVM2) → `ubuntu--vg-ubuntu--lv` ext4 `/`. + +## 4. Seal LUKS to the TPM (manual — PCR 7 only) + +Do this AFTER first boot. Manual enrollment sidesteps the installer's dbt profiler and lets us pick +PCRs. **Bind to PCR 7 only** (Secure Boot state): it catches the attack that matters (disabling +Secure Boot to boot a tampered kernel) WITHOUT breaking on routine kernel/GRUB updates (which churn +PCRs 4/8/9 → would otherwise drop every boot to the passphrase). Firmware-only PCR 0 is the fallback +if PCR 7 ever errors. + +```bash +sudo apt update && sudo apt install -y tpm2-tools +sudo tpm2_pcrread sha256 # sanity: PCRs 0-10 populated, PCR 7 has a real value + +# Enroll the TPM (prompts for the EXISTING install passphrase to authorize the new slot): +sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7 /dev/sda3 + +# Verify TWO slots — keep BOTH (slot 0 password = recovery, slot 1 tpm2 = auto-unlock): +sudo systemd-cryptenroll /dev/sda3 +# SLOT TYPE +# 0 password +# 1 tpm2 +``` + +Wire it into boot (back up first; the mapping is `dm_crypt-0`, the LUKS UUID is in `/etc/crypttab`): + +```bash +sudo cp /etc/crypttab /etc/crypttab.bak +sudo sed -i 's/none luks$/none luks,tpm2-device=auto/' /etc/crypttab +cat /etc/crypttab # → dm_crypt-0 UUID=… none luks,tpm2-device=auto +sudo update-initramfs -u +sudo reboot +``` + +- **Boots straight to login, no passphrase prompt** = ✅ TPM auto-unlock works (unattended reboot + achieved — VERIFIED on this unit 2026-06-23). +- Still prompts = PCR mismatch; type the passphrase (NOT locked out), then retry with + `--tpm2-pcrs=0`. The `password` slot + `crypttab.bak` make this fully reversible. + +> **Re-seal runbook:** a BIOS update / Secure Boot change alters PCR 7 → the TPM refuses → boot +> falls back to the passphrase prompt (not a brick). After such a change, re-run step 4's +> `systemd-cryptenroll --wipe-slot=tpm2 --tpm2-device=auto --tpm2-pcrs=7 /dev/sda3` to re-bind. + +## 5. GRUB password — EDIT-ONLY (VERIFIED 2026-06-23) + +Closes the `init=/bin/bash` / `systemd.unit=rescue.target` local-root hole: without it, anyone at +the keyboard presses `e` at the GRUB menu, edits the kernel cmdline, and boots to a **root shell with +no login**. **The PCR-7 TPM seal does NOT cover this** — editing the GRUB cmdline doesn't change +PCR 7 (Secure Boot policy), so the TPM still releases the key and the attacker lands on the decrypted +disk. This is the specific countermeasure for the [[threat-model|operator-at-the-booth]]. Use +**edit-only** mode (`--unrestricted`) so the box still boots UNATTENDED — the password is required +only to EDIT entries, never to boot. + +```bash +grub-mkpasswd-pbkdf2 # enter a password (twice) → copy the grub.pbkdf2.sha512.* hash +``` + +Add the superuser (paste YOUR hash) to the end of `/etc/grub.d/40_custom`: +``` +set superusers="admin" +password_pbkdf2 admin grub.pbkdf2.sha512.10000. +``` + +Make menu entries bootable WITHOUT the password (edit-only) — in `/etc/grub.d/10_linux`, set the +active `CLASS=` line to include `--unrestricted`: +``` +CLASS="--class gnu-linux --class gnu --class os --unrestricted" +``` + +Regenerate + VERIFY BOTH HALVES landed in the real config BEFORE rebooting (a GRUB misconfig means a +rescue-USB recovery): +```bash +sudo update-grub +sudo grep -c "password_pbkdf2" /boot/grub/grub.cfg # want ≥1 (password present) +sudo grep -c "unrestricted" /boot/grub/grub.cfg # want ≥1 (entries bootable w/o password) +sudo reboot +``` + +✅ VERIFIED on this unit: boots straight to login (no GRUB prompt, TPM still auto-unlocks) AND +pressing `e` at the menu prompts for `admin` + password. Store the GRUB password off-machine +(alongside the LUKS passphrase). + +> OS hardening on the first unit is now COMPLETE: LUKS FDE + TPM auto-unlock (PCR 7) + Secure Boot +> (Deployed) + GRUB edit-lock. + +## 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]]). +- 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. + +## 6. Runtime — Docker stack + +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. + +## Quick-reference: the gotchas, in order they bit us + +1. Ventoy USB → `0x1A` Security Violation under Secure Boot → flash the ISO directly instead. +2. 7070 BIOS has no "View Key" → can't inspect db; the live-USB boot IS the verification. +3. Installer "hardware-backed encryption" → `PCR_UNUSABLE`/dbt → use passphrase LUKS + manual seal. +4. Bind TPM to **PCR 7 only**, not a multi-PCR set (kernel updates churn 4/8/9 → passphrase every boot). +5. Always keep the **password slot** + an off-machine copy of the passphrase (TPM is never the only key). +6. GRUB password MUST be **edit-only** (`--unrestricted` on entries) or it prompts on EVERY boot → + breaks unattended reboot. Verify `grep -c unrestricted /boot/grub/grub.cfg` ≥1 before rebooting. diff --git a/wiki/index.md b/wiki/index.md index 646191b..bcd1f20 100644 --- a/wiki/index.md +++ b/wiki/index.md @@ -122,3 +122,4 @@ 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. +- [[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 507019a..a3742d2 100644 --- a/wiki/log.md +++ b/wiki/log.md @@ -1484,3 +1484,35 @@ insecure/change-me). VERIFIED: server image builds + runs — migrates, SPA serv / + /booth serve HTML, /api/nope JSON 404, no sqlite outside /data. Vision image build + smoke in progress. New page [[container-deployment]]; updated [[vision-service-packaging]] (resolved its two open Qs), index. Server tests stay 141 green (SPA serving guarded on dist existence). + +## [2026-06-23] provision | First booth appliance — Dell OptiPlex 7070, Win11 → Ubuntu 26.04 LTS, encrypted + TPM-sealed +Provisioned the first real booth PC. Hardware: Dell OptiPlex 7070, i5-8500, 238GB SSD, discrete +Nuvoton TPM 2.0 (NOT Intel PTT — Get-Tpm ManufacturerIdTxt NTC). Formatted Win11 → Ubuntu 26.04 LTS +(the decided platform). Gotchas hit + resolved, in order: (1) Ventoy USB → 0x1A Security Violation +under Secure Boot (Ventoy's loader not in db) → flash the ISO directly; (2) the 7070 BIOS Expert Key +Management is edit-only, no "View Key" → can't inspect db, so the live-USB boot IS the verification +(it reached the installer with Secure Boot ON → MS 3rd-party UEFI CA confirmed present); (3) the +installer's "Use hardware-backed encryption" FAILED with PCR_UNUSABLE / "secure boot policy (PCR7) … +timestamp revocation (dbt) … not supported" — Ubuntu's automated FDE profiler can't model PCR7 on +Dell firmware with a dbt; NOT a TPM/SB fault. Workaround: "Encrypt with a passphrase" (plain LUKS) + +MANUAL TPM seal after boot via systemd-cryptenroll --tpm2-pcrs=7 /dev/sda3 (PCR 7 only — avoids +kernel-churned 4/8/9 that would drop every boot to the passphrase). Two LUKS slots kept (0 password = +recovery, 1 tpm2 = auto-unlock); crypttab gets tpm2-device=auto; update-initramfs; reboot → BOOTS +STRAIGHT TO LOGIN, no passphrase → TPM auto-unlock VERIFIED (unattended reboot achieved). New runbook +page [[appliance-provisioning]] (every command verified on hardware); cross-linked from +[[disk-os-hardening]] (resolves impl half of open-questions #12 for unit 1) + index. REMAINING on the +box: GRUB password, Docker install, run the parking-server/parking-vision stack. + +## [2026-06-23] provision | First booth appliance — GRUB edit-lock added; OS hardening COMPLETE +Added the GRUB password (edit-only mode via --unrestricted) to the first booth unit. WHY it matters +specifically: the PCR-7 TPM seal does NOT cover the GRUB-cmdline attack (editing the kernel line to +init=/bin/bash doesn't change PCR 7, so the TPM still releases the LUKS key → root shell on the +decrypted disk). Edit-only mode keeps unattended boot (the box still boots password-free; the +password is required only to EDIT entries / open the GRUB shell) — the right config for an unattended +booth. Verified BOTH halves in /boot/grub/grub.cfg before rebooting (password_pbkdf2 ≥1, unrestricted +≥1) and on reboot: boots straight to login (no GRUB prompt, TPM auto-unlock intact) AND pressing `e` +prompts for admin+password. OS hardening on unit 1 is now COMPLETE: LUKS FDE + TPM auto-unlock (PCR 7) ++ Secure Boot (Deployed) + GRUB edit-lock. Updated [[appliance-provisioning]] (§5 GRUB now a verified +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).