Skip to content

fix(db): runtime Postgres roles no longer inherit the owner role - #1684

Merged
Makisuo merged 1 commit into
mainfrom
sec/1022b-pg-least-privilege
Oct 25, 2026
Merged

Makisuo merged 1 commit into
mainfrom
sec/1022b-pg-least-privilege

Conversation

@Makisuo

@Makisuo Makisuo commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Issue Fix Test
Runtime PlanetScale roles (ingest gateway, EU db-api-role, EU db-alerting-role) inherited postgres, which owns every table, so each service could alter or drop schema They inherit DB_ACCESS_ROLES.readWrite (pg_read_all_data + pg_write_all_data, new @maple/db/roles). DML on every current and future table and sequence; no DDL, TRUNCATE, ownership or role management. Migrations still run on alchemy's temporary postgres role packages/db/src/roles.test.ts: migrates the real schema in PGlite, then as each login checks reads, writes, sequence nextval, refusal of CREATE/DROP/ALTER/TRUNCATE/CREATE ROLE, and that a table added later is covered with no extra grant. With readWrite set back to ["postgres"] the 4 DDL-refusal cases fail
planetscale-connection.ts minted postgres-inheriting credentials by default for every data script withBranchConnection(branch, access, fn) takes an explicit level: read for audit-raw-sql, check-schema-drift, check-row-decoding and the restore drill (all current callers); readWrite is there for a future write script. The env override to postgres is gone Typecheck of every call site

Why built-in roles instead of per-table GRANTs: PlanetScale's role API only accepts its built-in role list for inherited_roles, and the role names it generates are not known when migrations run, so a migration cannot GRANT to them. pg_read_all_data/pg_write_all_data apply to tables created later, which covers the "future migrations" case without ALTER DEFAULT PRIVILEGES. A per-table matrix (below) is a follow-up if PlanetScale ever accepts custom inherited roles.

Electric is unchanged: PlanetScale grants REPLICATION only together with postgres (documented on the alchemy resource). Its publication is already owned by a migration (ELECTRIC_MANUAL_TABLE_PUBLISHING), so it does not need ownership for that.

Per-service inventory (from code)

Login Consumers Reads Writes DDL needed
ingest-gateway (ECS, pooled) apps/ingest org_ingest_keys, sources, org_clickhouse_settings, cloudflare_logpush_connectors, gcp_connectors, org_ingest_sampling_policies, org_ingest_attribute_mappings UPDATE sources, cloudflare_logpush_connectors, gcp_connectors none
db-api-role (EU Hyperdrive) api, ai, chat-bot, electric-sync app schema via packages/backend services app schema (incl. identity sequence, pg_advisory_xact_lock) none (grep of runtime code: no Postgres DDL, TRUNCATE, REFRESH or SET ROLE)
db-alerting-role (EU Hyperdrive) alerting alert rules/destinations/incidents/checks, org config alert state tables none
electric-db-role (ECS) Electric tables in electric_publication_default none (replication slot) none, but needs REPLICATION
migrations alchemy PostgresBranch all all yes (temporary postgres role, unchanged)

Breaking-change risk

  • Changing inheritedRoles is a create-first replace of each role: new credentials, the Hyperdrive configs and the ingest task env (MAPLE_PG_ROLE_ID) roll to them, then the old role is deleted with successor postgres. Brief dual-credential window, no downtime expected.
  • Anything at runtime that needs more than DML would now fail with permission denied. Code search found none; the EU deploy is the first real exercise.
  • pscale role create --inherited-roles pg_read_all_data,pg_write_all_data: pscale 0.332.0 role create --help documents the flag as a comma-separated list and names both roles as standard values. Not run against a database.
  • US prd api/alerting Hyperdrive configs are dashboard-managed (ref), so their credentials are untouched by this PR.

Verification

  • bun run --cwd packages/db test -- src/roles.test.ts: 21 passed; 4 fail with the old grant (re-run after rebasing onto main 2026-10-25, so the tables added since, e.g. the security-audit store, are covered).
  • Runtime code merged to main since the branch was cut has no Postgres DDL, TRUNCATE, REFRESH or SET ROLE.
  • bun run --cwd packages/db typecheck, tsrs -p tsconfig.alchemy.json, bun run lint: clean.
  • Nothing was run against any PlanetScale or Neon database.

Needs a human

Draft on purpose: this changes production database roles.

  1. A preview cannot validate this. Previews use the Neon branch owner for every consumer (declarePreviewDb), so a preview deploy only proves nothing else regressed. Still worth one: label preview, then check sign-in and a dashboard load (api), an alert rule preview and a tick (alerting), MCP list_services (ai), an OTLP post to ingest-pr-<n> returning 2xx (ingest), and a live-updating list (Electric).
  2. Deploy order. EU first (declared: all three runtime roles change). Watch the deploy create the new roles, update db-api/db-alerting Hyperdrive and roll the ingest tasks before deleting the old roles. Then verify on EU: API 2xx on dashboards/alerts, alerting ticks without permission denied in Maple's own logs, ingest key auth plus sources.last_used_at advancing, Electric shapes still syncing. Then US prd (only ingest-gateway changes); same ingest checks.
  3. US Hyperdrive credentials are outside the deploy; recreate them by hand with pg_read_all_data,pg_write_all_data if you want parity.
  4. Rollback: revert this commit and redeploy (another create-first replace back to postgres).
  5. Confirm on PlanetScale that pg_write_all_data is offered on these clusters (it is in the API enum alchemy ships).

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

Summary by CodeRabbit

  • Improvements
    • Production data-ingest and consumer connections now use explicit read/write access, while read-only database checks use read-only access.
    • Database roles retain access to tables added after their creation without gaining schema-change, truncation, or ownership permissions.
    • Schema changes remain restricted to migrations; the Electric connection’s existing access remains unchanged.

@coderabbitai

coderabbitai Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 7099e8a8-32dc-448f-9b60-d58084a717c2
Commits

Reviewing files that changed from the base of the PR and between b8f8105 and 9817bae.

Files selected for processing: 10
  • alchemy.run.ts
  • docs/infra.md
  • packages/db/package.json
  • packages/db/scripts/audit-raw-sql.ts
  • packages/db/scripts/check-row-decoding.ts
  • packages/db/scripts/check-schema-drift.ts
  • packages/db/scripts/planetscale-connection.ts
  • packages/db/scripts/restore-test.ts
  • packages/db/src/roles.test.ts
  • packages/db/src/roles.ts

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review.

Walkthrough

The change adds exported read and read-write database role mappings. Branch-connection scripts select an access level, and production ingest and per-consumer roles use the read-write mapping.

Changes: Database access roles

Layer / File(s) Summary
Access role definitions and checks
packages/db/src/roles.ts, packages/db/package.json, packages/db/src/roles.test.ts
The package exports read and read-write role mappings and tests the data and DDL permissions of roles that inherit them.
Access-scoped branch credentials
packages/db/scripts/planetscale-connection.ts, packages/db/scripts/audit-raw-sql.ts, packages/db/scripts/check-row-decoding.ts, packages/db/scripts/check-schema-drift.ts, packages/db/scripts/restore-test.ts
withBranchConnection requires an access level. Its callers pass read access, and credential creation uses the roles configured for that access level.
Runtime role configuration
alchemy.run.ts, docs/infra.md
Ingest and per-consumer roles inherit the read-write role set. The infrastructure notes describe runtime role permissions and the Electric and US production Hyperdrive exceptions.

Priority: Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Suggested labels: preview

Suggested reviewers: jeremyfunk

Merge Risk: Minimal · up to 9817b

No actionable merge-blocking defect was established. Confirm the production role transition during deployment.

Pre-merge checks: Passed 5
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the primary change: runtime PostgreSQL roles no longer inherit the owner role. It matches the pull request objectives and file changes.
Docstring Coverage Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 8…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 8 files. (2 skipped: 2 unsupported.)

Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The ingest gateway role and the EU api/alerting Hyperdrive roles inherited
`postgres`, which owns every table, so each runtime login could run DDL.
They now inherit pg_read_all_data + pg_write_all_data (DB_ACCESS_ROLES in
@maple/db/roles): DML on every current and future table, no DDL, TRUNCATE or
ownership. Migrations keep running on alchemy's temporary postgres role.
Electric stays on postgres since PlanetScale grants REPLICATION only with it.

The PlanetScale script broker now takes an explicit access level per call
(read for audits and the restore drill, readWrite for backfills) instead of
defaulting to postgres.
@Makisuo
Makisuo force-pushed the sec/1022b-pg-least-privilege branch from 4ca9937 to 9817bae Compare October 25, 2026 16:53
@Makisuo
Makisuo marked this pull request as ready for review October 25, 2026 16:53
@maple-review-bot

maple-review-bot Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Maple review

🟢 Confidence 9/10 · safe to merge
Privilege narrowing is pinned by a real-schema PGlite test, but the pscale --inherited-roles comma list is unverified until the EU deploy.
no findings · tests covered · risk high −1

Narrows every declared prd runtime Postgres login from inheriting postgres to the pg_read_all_data/pg_write_all_data built-ins, centralised in a new @maple/db/roles, with a PGlite test that migrates the real schema. Safe to merge; the residual risk is deploy-time, not in the diff.

  • DB_ACCESS_ROLES in @maple/db/roles replaces postgres inheritance for prd runtime logins
  • Ingest gateway and EU db-api/db-alerting roles now hold only pg_read_all_data + pg_write_all_data
  • withBranchConnection takes an explicit read/readWrite; the PLANETSCALE_MIGRATE_INHERITED_ROLES override is gone

Before merge

  • Infra · Check every environment has what this config now expects · alchemy.run.ts
  • Manual · Confirm pscale role create --inherited-roles pg_read_all_data,pg_write_all_data accepts a comma list before the prd deploy
  • Manual · Recreate the dashboard-managed US prd db-api-role and db-alerting-role by hand; this PR leaves them inheriting postgres
What was checked
  • No runtime Postgres DDL, TRUNCATE, RLS policy or publication code in packages/backend or apps/*/src at head
  • pg_write_all_data covers identity sequences: scrape_target_checks_id_seq exists in packages/db/drizzle and the test asserts nextval
  • All four withBranchConnection callers (audit, drift, row-decoding, restore drill) issue SELECTs only

9817bae · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Devin Review: 2 flags

Not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)

Devin Review

@Makisuo
Makisuo merged commit babbfca into main Oct 25, 2026
34 checks passed
@Makisuo
Makisuo deleted the sec/1022b-pg-least-privilege branch October 25, 2026 17:19
@maple-review-bot

Copy link
Copy Markdown

Merged with 3 steps from "Before merge" still open. If they are done, tick them on the review comment; if not, now is the time.

  • Infra · Check every environment has what this config now expects
  • Manual · Confirm pscale role create --inherited-roles pg_read_all_data,pg_write_all_data accepts a comma list before the prd deploy
  • Manual · Recreate the dashboard-managed US prd db-api-role and db-alerting-role by hand; this PR leaves them inheriting postgres

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant