§9 prod compose secrets (todo §9); the base compose.yml was already the full prod stack (web + Postgres + Kratos/Keto/Hydra + migrations + bootstrap, no source mount) but set REQUIRE_SECURE_SECRETS=true without ever passing CSRF_SECRET into web, so docker compose -f compose.yml up couldn't boot. Wired 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 prod-unset→reject, prod-set→real, dev→throwaway+toggle-off→boots). Used :- not :? because compose interpolates the base per-file before merging the dev override (confirmed empirically), so :? would also break the zero-config dev up. Tests-first: compose.test.ts guards secret-via-env + no-source-mount + prod/dev toggle split + postgres-creds-via-env. README prod section corrected (dropped the stale planned note). typecheck + 310 units green.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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.
|
||||
|
||||
2
todo.md
2
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.
|
||||
|
||||
Reference in New Issue
Block a user