From 66c12915782179772b30faf16867616f4b5bae74 Mon Sep 17 00:00:00 2001 From: Julian Cuni Date: Sun, 21 Jun 2026 23:52:21 +0200 Subject: [PATCH] docs(deploy): COOKIE_SECURE=0 runbook for the plain-HTTP appliance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents the deploy-time requirement that the cookie fail-safe fix (7629d5d) introduced: the LAN appliance serves the SPA same-origin over plain http, where a Secure cookie is never sent — so it MUST set COOKIE_SECURE=0 or operators can't log in. A TLS deploy leaves it unset. - wiki/concepts/disk-os-hardening.md: new "Deploy-time server configuration (runbook)" section listing the security-load-bearing env (JWT_SECRET, EVENT_SIGNING_KEY, COOKIE_SECURE=0) with the why + the network-scoped justification. - wiki/entities/local-jwt-auth.md: corrected the stale "Secure when NODE_ENV=production" cookie line to the Secure-by-default / opt-out model. - wiki/log.md: entry. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V --- wiki/concepts/disk-os-hardening.md | 20 ++++++++++++++++++++ wiki/entities/local-jwt-auth.md | 8 ++++++-- wiki/log.md | 11 +++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/wiki/concepts/disk-os-hardening.md b/wiki/concepts/disk-os-hardening.md index 348ce22..87021d7 100644 --- a/wiki/concepts/disk-os-hardening.md +++ b/wiki/concepts/disk-os-hardening.md @@ -28,3 +28,23 @@ Physical-access attacks on Windows are trivial (boot media + password-reset tool With LUKS in place, **SQLCipher becomes optional** defence-in-depth rather than the critical layer. (The custom controller adds its own: ESP32 flash encryption + secure boot — see [[esp32-custom-controller]].) + +## Deploy-time server configuration (runbook) + +Env in `apps/server/.env` on the appliance (see `apps/server/.env.example`). The security-load-bearing ones: + +- **`JWT_SECRET`** — ≥32 random chars; the server refuses to boot without a strong one (no + insecure default). `openssl rand -hex 32`. See [[local-jwt-auth]]. +- **`EVENT_SIGNING_KEY`** — dedicated HMAC key for the signed ledger; ≥16 chars. Falls back to + `JWT_SECRET` with a warning if unset — set a dedicated one before production. +- **`COOKIE_SECURE=0`** — **REQUIRED on the plain-HTTP LAN appliance.** Auth/CSRF cookies are + `Secure` by **default** (fail-safe). The appliance serves the SPA same-origin over **plain + http** on the booth LAN, where a `Secure` cookie is **never sent** — so without this opt-out + **operators cannot log in**. Set it deliberately. (A TLS/reverse-proxied deploy leaves it + UNSET so cookies stay `Secure`.) This replaced the old `NODE_ENV=production` gate, which + silently dropped `Secure` if the var was forgotten. See [[local-jwt-auth]]. + +> The plain-http booth LAN is acceptable because it's an **isolated, single-purpose network** +> (the only browser is the booth's own; access controllers sit on a separate VLAN — see +> [[network-isolation]], [[trust-boundary]]). `Secure`-off is a network-scoped decision, not a +> blanket weakening; the JWT stays HttpOnly + SameSite=Strict and CSRF double-submit still applies. diff --git a/wiki/entities/local-jwt-auth.md b/wiki/entities/local-jwt-auth.md index dd4b4aa..2a702ee 100644 --- a/wiki/entities/local-jwt-auth.md +++ b/wiki/entities/local-jwt-auth.md @@ -51,8 +51,12 @@ Authentication and authorization, kept **fully local** — a direct consequence The SPA never sees the JWT. Login (`POST /api/auth/login`) verifies bcrypt and sets two cookies: -- **`parking_token`** — the JWT, **HttpOnly + SameSite=Strict** (+ `Secure` when - `NODE_ENV=production`). JS can't read it; `@fastify/jwt` reads it from the cookie, not the +- **`parking_token`** — the JWT, **HttpOnly + SameSite=Strict**, and **`Secure` by default** + (fail-safe — a forgotten env can only make cookies more restrictive, never drop the flag). + `Secure` is dropped ONLY for a deliberate opt-out: `COOKIE_SECURE=0` (the plain-HTTP LAN + appliance — see [[disk-os-hardening]] deploy checklist) or `NODE_ENV=development`. (Was keyed + off `NODE_ENV=production`, which silently leaked cookies on an appliance that forgot to set it + — corrected 2026-06-21.) JS can't read it; `@fastify/jwt` reads it from the cookie, not the `Authorization` header. - **`parking_csrf`** — a random token, **readable** by JS. The JWT also carries a matching `csrf` claim. On every mutation the SPA echoes the cookie in the **`X-CSRF-Token`** header; the guard diff --git a/wiki/log.md b/wiki/log.md index b71d227..75863b7 100644 --- a/wiki/log.md +++ b/wiki/log.md @@ -1329,3 +1329,14 @@ autouse fixture that pins `VISION_RECOGNIZER=stub` (the dev `.env` had set `fast model-free smoke tests). Also stopped `*.test.ts` leaking into shipped `dist/` (server + shared tsconfig excludes). Totals: shared 87, server 75, devices 18, web 17, vision 7 = 204 tests; build/lint 14/14. See [[booth-console]]. + +## [2026-06-21] fix | Auth cookies Secure-by-default; COOKIE_SECURE=0 in the appliance deploy runbook +secureCookies() keyed off NODE_ENV==="production", so an appliance deployed without that var +silently dropped the Secure flag on the auth/CSRF cookies (the code-review's one Medium finding). +Flipped to FAIL-SAFE: Secure by DEFAULT, dropped only on a deliberate COOKIE_SECURE=0/false/no/off +(or NODE_ENV=development as a dev fallback). The plain-http LAN appliance sets COOKIE_SECURE=0 ON +PURPOSE (a Secure cookie is never sent over its http origin → operators couldn't log in); a TLS +deploy leaves it unset. Added a "Deploy-time server configuration (runbook)" section to +[[disk-os-hardening]] documenting COOKIE_SECURE=0 (+ JWT_SECRET / EVENT_SIGNING_KEY) and corrected +the stale "Secure when NODE_ENV=production" line on [[local-jwt-auth]]. auth.test.ts (5) pins the +matrix; server 80/80.