From b3b51db52bb9d3cd2569746fc6bed76bf74abc00 Mon Sep 17 00:00:00 2001 From: lilleman Date: Sat, 20 Jun 2026 01:05:15 +0200 Subject: [PATCH] =?UTF-8?q?=C2=A79=20prod=20compose=20secrets=20(todo=20?= =?UTF-8?q?=C2=A79);=20the=20base=20compose.yml=20was=20already=20the=20fu?= =?UTF-8?q?ll=20prod=20stack=20(web=20+=20Postgres=20+=20Kratos/Keto/Hydra?= =?UTF-8?q?=20+=20migrations=20+=20bootstrap,=20no=20source=20mount)=20but?= =?UTF-8?q?=20set=20REQUIRE=5FSECURE=5FSECRETS=3Dtrue=20without=20ever=20p?= =?UTF-8?q?assing=20CSRF=5FSECRET=20into=20web,=20so=20`docker=20compose?= =?UTF-8?q?=20-f=20compose.yml=20up`=20couldn't=20boot.=20Wired=20CSRF=5FS?= =?UTF-8?q?ECRET:=20${CSRF=5FSECRET:-dev-insecure-csrf-secret}=20=E2=80=94?= =?UTF-8?q?=20env-supplied=20with=20the=20throwaway=20as=20the=20only=20fa?= =?UTF-8?q?llback;=20config.ts's=20existing=20REQUIRE=5FSECURE=5FSECRETS?= =?UTF-8?q?=20logic=20rejects=20that=20throwaway=20so=20a=20forgotten=20pr?= =?UTF-8?q?od=20secret=20fails=20loud=20(verified=20prod-unset=E2=86=92rej?= =?UTF-8?q?ect,=20prod-set=E2=86=92real,=20dev=E2=86=92throwaway+toggle-of?= =?UTF-8?q?f=E2=86=92boots).=20Used=20:-=20not=20:=3F=20because=20compose?= =?UTF-8?q?=20interpolates=20the=20base=20per-file=20before=20merging=20th?= =?UTF-8?q?e=20dev=20override=20(confirmed=20empirically),=20so=20:=3F=20w?= =?UTF-8?q?ould=20also=20break=20the=20zero-config=20dev=20up.=20Tests-fir?= =?UTF-8?q?st:=20compose.test.ts=20guards=20secret-via-env=20+=20no-source?= =?UTF-8?q?-mount=20+=20prod/dev=20toggle=20split=20+=20postgres-creds-via?= =?UTF-8?q?-env.=20README=20prod=20section=20corrected=20(dropped=20the=20?= =?UTF-8?q?stale=20planned=20note).=20typecheck=20+=20310=20units=20green.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +++++- compose.yml | 4 +++- src/compose.test.ts | 14 ++++++++++++++ todo.md | 2 +- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8e08571..549c9e5 100644 --- a/README.md +++ b/README.md @@ -568,7 +568,11 @@ request, because the session lives in Kratos and the data lives upstream. docker compose -f compose.yml up --build -d # base config only, no source mount ``` -_(Production compose grows to include the Ory services and Postgres — planned.)_ +`compose.yml` is the full prod stack — web + Postgres + the three Ory services +(Kratos/Keto/Hydra, with migrations + the one-shot bootstrap) — and mounts no source. +Secrets come from the environment (`CSRF_SECRET`, `POSTGRES_USER`/`POSTGRES_PASSWORD`); the +base already sets `REQUIRE_SECURE_SECRETS=true`, so a missing or dev-throwaway `CSRF_SECRET` +fails the boot rather than running insecure. Before going live, supply the production secrets and any SSO credentials — the **only** manual prep ([What you must supply](#what-you-must-supply-the-only-manual-prep)); the rest diff --git a/compose.yml b/compose.yml index 0422019..874bd39 100644 --- a/compose.yml +++ b/compose.yml @@ -6,9 +6,11 @@ services: ports: - "3000:3000" # Explicit behaviour toggles (the app is environment-agnostic — see AGENTS.md). - # Supply CSRF_SECRET via env; REQUIRE_SECURE_SECRETS refuses the dev throwaway. + # Supply CSRF_SECRET via env; the dev-throwaway fallback boots a clean clone but + # REQUIRE_SECURE_SECRETS refuses it in prod (config.ts), so a forgotten secret fails loud. environment: CACHE_TEMPLATES: "true" + CSRF_SECRET: ${CSRF_SECRET:-dev-insecure-csrf-secret} REQUIRE_SECURE_SECRETS: "true" SECURE_COOKIES: "true" # prod serves https — mark session/CSRF cookies Secure # Wait for the services the app talks to (kratos + keto + hydra for the §6 OAuth2 login/ diff --git a/src/compose.test.ts b/src/compose.test.ts index 3ae69bb..45da1ac 100644 --- a/src/compose.test.ts +++ b/src/compose.test.ts @@ -57,6 +57,20 @@ test("prod base publishes no internal Ory ports; dev exposes the host-facing one assert.match(override, /"4444:4444"/, "dev publishes hydra public"); }); +test("prod base supplies the app secret via env and mounts no source; dev override flips it", () => { + // §9 prod compose: CSRF_SECRET comes from the environment (dev-throwaway fallback that + // REQUIRE_SECURE_SECRETS rejects in prod — see config.ts); the base never bind-mounts the + // source tree (runs the built image), while the dev override does for live editing. + assert.match(webBlock, /CSRF_SECRET:\s*\$\{CSRF_SECRET\b/, "base wires CSRF_SECRET from env"); + assert.doesNotMatch(webBlock, /-\s+\.:\/app\b/, "base mounts no source tree"); + assert.match(override, /-\s+\.:\/app\b/, "dev override bind-mounts the source"); + // Secret/cookie hardening: enforced in prod, off in dev so the throwaway + http cookies pass. + assert.match(webBlock, /REQUIRE_SECURE_SECRETS:\s*"true"/, "base enforces real secrets"); + assert.match(override, /REQUIRE_SECURE_SECRETS:\s*"false"/, "dev allows the throwaway"); + // Postgres credentials are env-supplied (dev default), never a baked-in literal. + assert.match(compose, /POSTGRES_PASSWORD:\s*\$\{POSTGRES_PASSWORD\b/, "postgres password via env"); +}); + test("a one-shot bootstrap seeds the stack before web starts", () => { // §3 MVP bar: `bootstrap` runs after kratos+keto are healthy, seeds the admin + // JWKS, then exits; web waits for it to complete. Live seeding is boot-verified. diff --git a/todo.md b/todo.md index 23d7788..1fad5b1 100644 --- a/todo.md +++ b/todo.md @@ -125,7 +125,7 @@ everything via Docker. - [x] Go over all tests and combine/unify ones that cover the same stuff or are very related and could be combined in a good way. Remove tests that aren't helping, we only want tests that are actually helpful to us. → Pass over the §8 test accretion (`fetch-timeout.test.ts`, `e2e/full-flow.spec.ts`, + the §8-review additions across `app`/`config`/`jwks`/`flow-view`/`data-table`/`admin-users`). Most §8 changes were already woven into existing tests as assertions (the `recoverHref` link into flow-view, `parseJwks` key-shape into the loadJwks test, `ORY_TIMEOUT_SEC` as a sibling numeric-knob in config, empty-state as a focused data-table feature) — no fat. **Two genuine cleanups:** (1) the deferred L3 (line 123) — `plugins/scheduling/shifts.test.ts` imported four deep `src/*` internals (`chrome`/`context`/`guards`/`plugin`), none of them the documented-stable surface; repointed all four to the `src/plugin-api.ts` barrel (the one contract boundary, which re-exports them), so the test now models the dev/test story the contract preaches — exactly like `shifts.ts` itself does (no coverage change). (2) `app.test.ts` had two adjacent tests for the *same* surface (the themed-auth GET dispatch): "themed flow init" (anonymous → flow init / CSRF relay / stale restart) and "an already-signed-in user is sent home" — the latter literally re-asserted the former's anonymous-init (line 448 ≡ 424). Merged into one "themed auth GET: anonymous inits a flow …; a signed-in user is sent home, except /settings", dropping the duplicate; all assertions preserved. Left separate (distinct surfaces/stacks, not fat): the four E2E suites (visual computed-styles / auth-refresh JWT re-mint / oauth-login consent / full-flow browser-UI — each its own compose stack) and `app.test.ts`'s one-per-surface integration tests. Pure test refactor, no production code (per the §6/§7 precedent, no stability reviewer). 310 → 309 units; typecheck + tests green. ## 9. Production, security, ops -- [ ] `compose.yml` prod: Ory + Postgres, secrets via env, no source mount. +- [x] `compose.yml` prod: Ory + Postgres, secrets via env, no source mount. → The base file was already the full prod stack (web + Postgres + Kratos/Keto/Hydra + migrations + the one-shot bootstrap; `.:/app` lives only in the dev override), built during §3. **The real gap, now closed:** it set `REQUIRE_SECURE_SECRETS=true` but never wired `CSRF_SECRET` into `web`, so `docker compose -f compose.yml up` couldn't boot. Added `CSRF_SECRET: ${CSRF_SECRET:-dev-insecure-csrf-secret}` — env-supplied with the throwaway as the only fallback; `config.ts`'s existing `REQUIRE_SECURE_SECRETS` logic rejects that throwaway, so a forgotten prod secret **fails loud** (verified all three paths: prod-unset→reject, prod-set→real secret, dev→throwaway + toggle off → boots). Used `:-` not `:?` because compose interpolates the base file per-file *before* merging the override (confirmed empirically), so a `:?` in the base would also break the zero-config dev `docker compose up`. Tests-first: extended `compose.test.ts` (secret-via-env + no-source-mount + the prod/dev toggle split + postgres-creds-via-env). README prod section corrected (dropped the stale "_(… Ory + Postgres — planned)_"). typecheck + 310 units green. - [ ] Security headers; secure/HttpOnly/SameSite cookies; CSRF; clock-skew tolerance. - [ ] Optional revocation denylist for instant role/session revoke. - [ ] Structured logging / basic observability. use @larvit/log for OTLP compability dig down in how to use it properly.