The relay no longer connects to Postgres as a superuser (relay#15) #108

Merged
paulhitt merged 1 commits from security/scoped-database-role into main 2026-09-04 21:46:17 +00:00
paulhitt commented 2026-09-04 21:45:56 +00:00 (Migrated from gitlab.com)

A SQL flaw in the relay used to be remote code execution on the database
host: superuser can run COPY … TO PROGRAM. That is the whole reason
this was worth doing regardless of confidence in the query layer.

The bootstrap superuser CANNOT be demoted — Postgres refuses
ALTER ROLE flamenet NOSUPERUSER outright — so this is a new scoped role
and a switchover, not a demotion.

The role OWNS its tables rather than holding only DML grants. Fluent
migrates at boot, so a role that cannot create a table would fail the next
deploy that carries a migration — and that failure reads as an application
bug rather than a missing grant.

Cross-database access needed a separate REVOKE CONNECT … FROM PUBLIC;
Postgres grants it by default, so the scoped role could still read
pg_catalog in the other database until that was done.

Rehearsed against the real schema AND the real migration rows before being
applied. The first rehearsal failed with "relation devices already
exists" — --schema-only carries that table but none of its rows, so
Fluent tried to create everything again. A rehearsal missing the
bookkeeping is not a rehearsal.

Applied to production and verified: RCE, role creation and the other
database all refused; relay healthy; edge answering 200.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

A SQL flaw in the relay used to be remote code execution on the database host: superuser can run `COPY … TO PROGRAM`. That is the whole reason this was worth doing regardless of confidence in the query layer. The bootstrap superuser CANNOT be demoted — Postgres refuses `ALTER ROLE flamenet NOSUPERUSER` outright — so this is a new scoped role and a switchover, not a demotion. The role OWNS its tables rather than holding only DML grants. Fluent migrates at boot, so a role that cannot create a table would fail the next deploy that carries a migration — and that failure reads as an application bug rather than a missing grant. Cross-database access needed a separate `REVOKE CONNECT … FROM PUBLIC`; Postgres grants it by default, so the scoped role could still read `pg_catalog` in the other database until that was done. Rehearsed against the real schema AND the real migration rows before being applied. The first rehearsal failed with "relation devices already exists" — `--schema-only` carries that table but none of its rows, so Fluent tried to create everything again. A rehearsal missing the bookkeeping is not a rehearsal. Applied to production and verified: RCE, role creation and the other database all refused; relay healthy; edge answering 200. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
paulhitt commented 2026-09-04 21:45:56 +00:00 (Migrated from gitlab.com)

assigned to @paulhitt

assigned to @paulhitt
paulhitt commented 2026-09-04 21:46:17 +00:00 (Migrated from gitlab.com)

mentioned in commit b75c01e217

mentioned in commit b75c01e217f3a0dbd2863b8827e2f32ddbcee2f3
paulhitt (Migrated from gitlab.com) merged commit b75c01e217 into main 2026-09-04 21:46:17 +00:00
paulhitt commented 2026-09-04 21:46:18 +00:00 (Migrated from gitlab.com)

mentioned in issue #15

mentioned in issue #15
paulhitt commented 2026-09-09 18:59:03 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
Sign in to join this conversation.