Repository navigation
Deploy: separate the migration/owner DB role from the runtime app role (no DDL at request-time) #263
Description
Activity
- addedpriority:criticalBlocks the vertical sliceBlocks the vertical slicearea:apiAPI/endpoint layerAPI/endpoint layer
on Jul 28, 2026 - added a commit that references this issue
on Jul 30, 2026 App-side landed in PR #288 (merged): the
migraterun-then-exit CLI verb,appsettings.Production.jsonwithDatabase:MigrateOnStartup=falseso the request-serving process never runs DDL, the compose one-shotmigrateservice thatappwaits on (service_completed_successfully), and the/health/ready503-while-pending backstop. The CLI verbs were also extracted into a testable command layer (Cli/) in the same PR.This does not close #263. The remaining work is host-specific and gated on the hosting decision (#244): create a least-privilege runtime DB role (
GRANT USAGE ON SCHEMA public+ DML/sequence privileges, no DDL), addALTER DEFAULT PRIVILEGESfor the migrator role so future-migration objects auto-grant to the runtime role, and point themigratejob at a separate owner/migrator credential. Keeping this open to track that role split.Reopening — this was auto-closed by accident. GitHub's keyword parser matched the string
closes #263inside the #288 squash-merge commit message, in a sentence that actually meant the opposite ("role split is the host follow-up that closes #263" — i.e. not yet done).#288 delivered only the app-side (migrate verb,
MigrateOnStartup=false, migrate-then-serve compose topology,/health/readybackstop). The actual privilege separation — a least-privilege runtime role (GRANT USAGE+DML, no DDL),ALTER DEFAULT PRIVILEGESfor the migrator, and a separate owner/migrator credential for the migrate job — is a host-specific deploy step that hasn't been done (the compose stack still uses one owner credential for both). Per AGENTS.md, the role split is what closes this. Keeping it open under the #244 audit until then.- added a commit that references this issue
on Jul 30, 2026 Closing as a duplicate of the deploy-side work now tracked in a separate deployment/ops repo (least-privilege runtime DB role + separate migrator credential).
The app-side is complete and merged (PR #288): the
migraterun-then-exit verb,MigrateOnStartup=falsein Production, migrate-before-serve ordering, and the/health/readybackstop. The remaining privilege separation (create the runtime role,ALTER DEFAULT PRIVILEGES, point the migrate job at an owner credential) is host DB provisioning and lives in that separate deployment repo.- added 7 commits that reference this issue
on Jul 30, 2026 - added a commit that references this issue
on Sep 6, 2026 - added a commit that references this issue
on Oct 7, 2026
Deployment-readiness gap (#244), BLOCKER (codex) / high (agents).
Problem
One database credential does everything. The runtime connection serves every request context (Program.cs:192) and runs schema DDL at boot, because
Database:MigrateOnStartupdefaults true (Program.cs:523-524). Compose uses the samePOSTGRES_USER(image owner/superuser) for the app and the database (docker-compose.yml). So a compromised API process — an RCE or a missed injection sink — holds DDL-level privilege and canDROP/ALTERthe schema, not merely read/write rows.Fix
Database:MigrateOnStartup=falsein production.USAGEon the schema + DML/sequence privileges — no DDL.Pairs naturally with the #245 migration squash (one migrator run per deploy).
Verify
Prove the runtime role cannot
CREATE/ALTER/DROPschema objects (attempt one, expect a permission error), while the app still serves reads/writes.Part of the #244 deployment-readiness audit; tracked in epic #15.