Recommended secure provisioning of a custom PostgreSQL LOGIN role on Supabase #51499
|
Hi Supabase community, I'm building a small, private application using Supabase Postgres. Its backend needs a dedicated, least-privilege PostgreSQL LOGIN role to execute narrowly scoped synchronization transactions. The intended connection is through the shared Supavisor transaction pooler (port 6543). I've already verified connectivity using My question concerns secure credential provisioning, not database connectivity. With PostgreSQL's I'd appreciate guidance on:
I'm specifically trying to distinguish between protecting plaintext passwords and protecting SCRAM verifiers, without introducing unnecessary complexity. I'm using the Supabase Free plan and would prefer a standard, production-supported solution rather than a temporary workaround. Thanks for any guidance or relevant documentation! |
Replies: 2 comments 2 replies
|
Yes to the pooler part first, since it is the simplest: custom roles through the shared pooler are supported. The connection docs say the shared pooler username is postgres.[PROJECT-REF], and if you connect as a custom role the username is [ROLE].[PROJECT-REF]. Port 6543 on the shared pooler is Supavisor in transaction mode. Least privilege is just standard Postgres: the role only has what you grant it. For creating and rotating the password, the supported path is SQL run as the postgres role (SQL editor, CLI, or a direct/psql connection). Supabase recommends a separate user per service rather than sharing the postgres password, and this is exactly that pattern: CREATE ROLE app_login WITH LOGIN PASSWORD '...'; For rotation, prefer psql's \password over writing the password into SQL: \password app_login The reason is in the Postgres CREATE ROLE docs. Postgres always stores the password encrypted in the catalogs (if you hand it a SCRAM verifier string, it stores that as-is), but a literal password inside CREATE ROLE or ALTER ROLE is sent in cleartext and can end up in the server log if statement logging catches it. \password exists specifically to change a password safely later without putting the plaintext in a statement. That also frames your verifier question. What Postgres stores is the SCRAM verifier, not the plaintext. The realistic exposure is the statement itself: if a CREATE/ALTER ROLE statement carrying a password or verifier is captured by statement logging anywhere, treat that captured text as a secret and rotate. Practically: set and rotate the password interactively with \password over your verified TLS connection, do not put role passwords in migration files or scripts that get logged, and grant the role only the permissions it needs so a leak has a small blast radius. One small note: the dashboard password setting only covers the built-in postgres role. Custom roles are managed with SQL, which is normal Postgres and fully supported on hosted projects. |
|
Adding to @aniketatgithub's answer — on the verifier-logging part specifically (your Q2/Q3), because the threat model there is worth being precise about. What a SCRAM verifier leak actually is — and isn't The verifier ( Where the verifier can and cannot end up on hosted Supabase
The leak vector that actually bites: migration files The classic mistake isn't logs — it's committing credential provisioning to the repo: -- supabase/migrations/0004_create_sync_role.sql ← DO NOT do this
create role app_sync login password 'SCRAM-SHA-256$4096:...';Now the verifier is in git history forever, in every clone, and in your deployment logs. Role creation and rotation should be operational actions (CLI session, secrets manager), never migration-file contents. What belongs in a migration is everything after the credential: the grants, the RLS, the Rotation with zero downtime Postgres doesn't support two live passwords for one role, so the zero-downtime pattern is a brief dual-role deploy:
For a small private app, quarterly-or-on-suspicion rotation is fine; the SCRAM iteration cost (4096 default) makes offline brute-force of a long random password impractical regardless. One transaction-pooler caveat for your sync transactions (Q4) Custom roles work through the shared 6543 pooler, as covered above — but transaction mode means no session-level state survives between queries. Two practical consequences for a sync worker:
The one-sentence version: rotate from an interactive session (never a committed migration), treat verifiers as brute-force-resilient but repo-toxic, and let revoked-everything roles make the credential low-value even if it leaks. |
Yes to the pooler part first, since it is the simplest: custom roles through the shared pooler are supported. The connection docs say the shared pooler username is postgres.[PROJECT-REF], and if you connect as a custom role the username is [ROLE].[PROJECT-REF]. Port 6543 on the shared pooler is Supavisor in transaction mode. Least privilege is just standard Postgres: the role only has what you grant it.
For creating and rotating the password, the supported path is SQL run as the postgres role (SQL editor, CLI, or a direct/psql connection). Supabase recommends a separate user per service rather than sharing the postgres password, and this is exactly that pattern:
CREATE ROLE app_login WITH…