feat(db): run the app as a role that cannot change the schema

After the migrations the entrypoint ensures healthlog_app, a login role
with row access to the app's schema, MAINTAIN on measurements and
ownership of the pgboss schema, and starts the app with a DATABASE_URL
for it in place of the owner's. The role cannot alter or drop tables,
create roles, schemas or functions in the app's schema, read server
files or run programs, or write the migration history.

The password is an HMAC of a fixed label under API_TOKEN_HMAC_KEY, so a
separate worker container derives the same one, and it is sent as a
SCRAM verifier. Every boot re-applies the grants in one transaction
under an advisory lock, and drops as the owner the indexes the
maintenance job may no longer drop. When the owner cannot create roles,
the server is older than Postgres 17, or the role cannot log in, the
boot says why and the app stays on the owner as before. DB_APP_ROLE=off
skips it.
This commit is contained in:
Marc-André Bombeck committed 2026-11-03 20:36:37 +01:00
1 parent 95525a0259
commit 485161a657
5 files changed
+1059 -3

No files matched your search

@@ -74,9 +74,11 @@ function entrypointScripts(): string[] {
const entrypoint = readFileSync(join(ROOT, "docker-entrypoint.sh"), "utf8");
return [
...new Set(
[...entrypoint.matchAll(/^\s*node\s+(scripts\/[\w.-]+)/gm)].map(
(m) => m[1],
),
// A line that starts with the call, or a command substitution that
// makes it (`url="$(node scripts/db-app-role.mjs …)"`).
[
...entrypoint.matchAll(/(?:^\s*|\$\(\s*)node\s+(scripts\/[\w.-]+)/gm),
].map((m) => m[1]),
),
].sort();
}
@@ -126,6 +128,7 @@ describe("scripts the entrypoint runs ship in the image", () => {
it("finds the boot scripts it is meant to check", () => {
// An empty match would agree with any Dockerfile.
expect(entrypointScripts()).toContain("scripts/pending-migrations.mjs");
expect(entrypointScripts()).toContain("scripts/db-app-role.mjs");
});
it("copies every boot script and its local imports into the runner stage", () => {