docs(wiki): appliance provisioning runbook — booth unit 1 hardened (LUKS+TPM+SecureBoot+GRUB)
CI / check (push) Successful in 45s

New wiki/decisions/appliance-provisioning.md: the hardware-verified step-by-step for
provisioning a booth PC (Dell OptiPlex 7070, i5-8500, discrete Nuvoton TPM 2.0) from
factory Windows to a hardened Ubuntu 26.04 LTS appliance. Every command was run on the
first real unit (2026-06-23). Captures the firmware-specific gotchas: Ventoy → 0x1A under
Secure Boot (flash ISO directly); the 7070 BIOS can't view db (verify via live USB); the
installer's hardware-backed encryption fails with PCR_UNUSABLE/dbt (use passphrase LUKS +
manual systemd-cryptenroll PCR-7 seal); GRUB password must be edit-only (--unrestricted)
to keep unattended boot.

OS hardening on unit 1 is COMPLETE + verified: LUKS FDE + TPM auto-unlock (PCR 7,
unattended) + Secure Boot (Deployed) + GRUB edit-lock (closes 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.

Cross-linked from disk-os-hardening; index + log updated. Still TODO on the box: Docker +
run the stack.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-06-23 15:53:56 +02:00
parent 8155ff456b
commit 9d65099d9b
4 changed files with 215 additions and 0 deletions
+8
View File
@@ -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 layer. (The custom controller adds its own: ESP32 flash encryption + secure boot — see
[[esp32-custom-controller]].) [[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) ## Deploy-time server configuration (runbook)
Env in `apps/server/.env` on the appliance (see `apps/server/.env.example`). The security-load-bearing ones: Env in `apps/server/.env` on the appliance (see `apps/server/.env.example`). The security-load-bearing ones:
+174
View File
@@ -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.<YOUR_HASH>
```
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.
+1
View File
@@ -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). - [[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. - [[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. - [[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.
+32
View File
@@ -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 / + /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 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). 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).