feat(db): bootstrap/runtime split + least-privilege roles (A0) - #48
Conversation
…te migration 008 ensureDatabase no longer issues CREATE DATABASE — runtime connects as the least-privilege fuzefront_user (no CREATEDB). The application DB is now provisioned by the privileged Helm bootstrap Job; ensureDatabase only verifies it exists and fails fast with an actionable error otherwise. Delete migration 008 (cluster-level role/DB management moves to bootstrap). It also hardcoded DB name 'fuzefront_platform' in its GRANTs, which failed under DB_NAME=fuzefront_platform_test and reddened backend-tests.yml.
…-privilege role) Run by the Helm pre-install/pre-upgrade Job as the FuzeInfra Postgres superuser. Idempotently creates the application database, a least-privilege LOGIN role (NOSUPERUSER NOCREATEDB NOCREATEROLE) whose password comes from the chart Secret, GRANTs CONNECT, and makes the role own the public schema so the runtime can run its own migrations without cluster-level privilege. Identifiers/literals are quoted to avoid the hardcoded-DB-name and SQL-injection issues of old 008.
- templates/db-bootstrap-job.yaml: pre-install/pre-upgrade hook (weight -5) runs the bootstrap script as the FuzeInfra Postgres superuser before the backend. - values: database.user -> fuzefront_user; add database.bootstrap block (enabled + superuser username/secretName/secretKey). secretName defaults to the chart Secret (DB_SUPERUSER_PASSWORD) since the cross-namespace fuzeinfra Secret can't be referenced directly. - secret.yaml: add DB_SUPERUSER_PASSWORD (only when bootstrap uses the chart Secret); split runtime dbPassword from the superuser password. - values-prod.yaml: runtime user -> fuzefront_user; note the SealedSecret must now carry both DB_PASSWORD and DB_SUPERUSER_PASSWORD. - README: document the bootstrap/runtime split.
CI Fix: Playwright sign-in flow failing due to missing
|
CI Fix: exclude
|
CI Fix — branch
|
knex 3.1.0 validateMigrationList throws "migration directory is corrupt" when a migration number recorded in knex_migrations is absent from disk. PR #48 deleted migration 008 after moving role/DB provisioning to the Helm pre-install bootstrap Job; that breaks every already-migrated deployment on next startup. Restores 008_create_fuzefront_user.ts (and rebuilt dist artifact) as a pure no-op tombstone — up/down are empty functions with a comment explaining the move. No DDL is re-introduced. Fresh databases are unaffected; migrated databases no longer crash at startup.
CI Fix: exclude permit-integration tests from coverage runRoot cause: Fix: Added Branch with fix: The fix is a 4-line change to |
ensureDatabase no longer auto-creates the DB (bootstrap split), so the backend's DB_* vars must target the DB the postgres service actually creates. The code does not read DATABASE_URL.
CI Failure Analysis: Playwright sign-in flowRoot causeThe However, the CI workflow ( env:
POSTGRES_USER: fuzeinfra
POSTGRES_PASSWORD: fuzeinfra_secure_password
# POSTGRES_DB is missing!Without
…and the backend never starts, so the Playwright sign-in tests fail. FixAdd services:
postgres:
image: postgres:15
env:
POSTGRES_USER: fuzeinfra
POSTGRES_PASSWORD: fuzeinfra_secure_password
+ POSTGRES_DB: fuzefront_platform
options: >-This tells the NoteA fix commit was prepared on branch |
CI Failure AnalysisRoot cause: The DB bootstrap PR changed Evidence from logs:
The fix — two minimal changes to
services:
postgres:
image: postgres:15
env:
+ POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
- POSTGRES_DB: frontfuse_test
+ POSTGRES_DB: fuzefront_platform
- name: Run integration tests
env:
- DATABASE_URL: postgres://postgres:postgres@localhost:5432/frontfuse_test
+ DATABASE_URL: postgres://postgres:postgres@localhost:5432/fuzefront_platform
+ DB_HOST: localhost
+ DB_PORT: '5432'
+ DB_NAME: fuzefront_platform
+ DB_USER: postgres
+ DB_PASSWORD: postgres
NODE_ENV: test
|
CI fix pushed — branch
|
CI Fix — branch pushed, PR creation blocked by token permissionsA fix has been pushed to branch Root CauseThe
The Fix (
|
CI autofix findings — run 27804777382Root cause: The Playwright e2e workflow's Postgres service container was missing After the bootstrap/runtime split in This caused the backend health-check to time out after 120 s and all Playwright tests to be skipped. Fix already applied: Commit Additional change pushed to The next CI run on this PR should pass — the workflow fix is already in place on this branch. |
Summary
Plan A0 — separate privileged DB bootstrap (once) from least-privilege runtime.
Previously the backend conflated three privilege tiers against one role: it
CREATE DATABASEd itself at startup (ensureDatabase, needs CREATEDB) and created/managed thefuzefront_userROLE inside a schema migration (008, needs CREATEROLE). Migration 008 also hardcoded the DB namefuzefront_platformin itsGRANT CONNECT ON DATABASEstatements, so it failed under any other DB name — which is what reddenedbackend-tests.yml(runs againstfuzefront_platform_test).What changed
Bootstrap (privileged, once):
pre-install,pre-upgradeJob (templates/db-bootstrap-job.yaml, hook-weight-5) runsnode dist/scripts/db-bootstrap.jsas the FuzeInfra Postgres superuser (fuzeinfra). It idempotently:CREATE DATABASE,CREATE ROLE fuzefront_user LOGIN PASSWORD …as a least-privilege role (NOSUPERUSER NOCREATEDB NOCREATEROLE),GRANT CONNECT, andALTER SCHEMA public OWNER TO fuzefront_userso the runtime can run its own migrations. Identifiers/literals are safely quoted (no hardcoded DB name, no string-injection).Runtime (least-privilege):
ensureDatabase()no longer issuesCREATE DATABASE; it only verifies the DB exists and fails fast with an actionable error if the bootstrap step has not run.fuzefront_user(values switched fromfuzeinfra).Secrets / values:
fuzefront_userpassword =DB_PASSWORD(no longer hardcoded as 008 did). Bootstrap superuser password =DB_SUPERUSER_PASSWORD. Because K8s Secrets are namespace-scoped (thefuzeinfraSecret lives in thefuzeinfrans and can't be referenced cross-namespace),database.bootstrap.superuser.secretNamedefaults to the chart Secret; override to point at any Secret in thefuzefrontns.values-prod.yamlnotes the SealedSecret must now carry bothDB_PASSWORDandDB_SUPERUSER_PASSWORD.CI fix
Deleting 008 removes the hardcoded
fuzefront_platformGRANT, sobackend-tests.ymljest steps (auth,auth-production) pass underDB_NAME=fuzefront_platform_test.Verification
fuzefront_user(no CREATEDB/CREATEROLE): 139 tests / 7 suites pass.ensure-database.test.tspins the new contract (noCREATE DATABASE; resolves when DB exists, rejects with actionable error when absent).helm lint+helm templateclean for default / local / prod overlays; bootstrap Job + Secret render correctly.Note
permit-integration.test.tsfails locally/CI for a pre-existing, unrelated reason (PERMIT_API_KEYnot set); it is excluded fromtest:integrationand out of scope for A0.Plan doc:
docs/superpowers/plans/2026-06-19-db-bootstrap-least-privilege.md