diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 3e9a854c652..776fcec5926 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -2897,6 +2897,7 @@ export const self_hosting: NavMenuConstant = { { name: 'Configure Phone Login & MFA', url: '/guides/self-hosting/self-hosted-phone-mfa' }, { name: 'Configure SAML 2.0 SSO', url: '/guides/self-hosting/self-hosted-saml-sso' }, { name: 'Enable MCP server', url: '/guides/self-hosting/enable-mcp' }, + { name: 'Remove superuser access', url: '/guides/self-hosting/remove-superuser-access' }, ], }, { diff --git a/apps/docs/content/guides/self-hosting/remove-superuser-access.mdx b/apps/docs/content/guides/self-hosting/remove-superuser-access.mdx new file mode 100644 index 00000000000..a2b7c6d6d8d --- /dev/null +++ b/apps/docs/content/guides/self-hosting/remove-superuser-access.mdx @@ -0,0 +1,75 @@ +--- +title: 'Remove superuser access from Studio' +description: 'Learn how to switch from the supabase_admin to postgres role in self-hosted Supabase.' +subtitle: 'Learn how to switch from the supabase_admin to postgres role in self-hosted Supabase.' +--- + +## Overview + +In late 2022, Supabase introduced a security change in hosted projects that removed superuser access from the dashboard SQL editor and shifted ownership of user-created database objects away from `supabase_admin` toward the `postgres` role. +You can read more about it in the [official announcement](https://github.com/orgs/supabase/discussions/9314). + +However, this migration was never automatically applied to self-hosted Supabase instances. + +As a result: + +- Objects created via the dashboard may still be owned by `supabase_admin` +- Behavior differs from the Supabase platform +- Some migrations may fail when run as `postgres` + +This guide explains how to align your self-hosted Supabase instance with the security enhancements and ownership model used on the Supabase platform. + +## Changing the configuration + +### Step 1: Update database object ownership + +Use the provided script to reassign ownership of database objects in the `public` schema from `supabase_admin` to `postgres`. From the project directory containing `docker-compose.yml`, run: + +```sh +sh utils/reassign-owner.sh +``` + + + +This script only updates ownership for database objects in the `public` schema. Supabase-managed and custom schemas are not affected. + + + +### Step 2: Update environment variables in docker-compose.yml + +- In your `docker-compose.yml` configuration, uncomment the following line for the `studio` service to use the `postgres` role for read/write operations: + + ```yml name=docker-compose.yml + studio: + environment: + POSTGRES_USER_READ_WRITE: postgres + ``` + +- Locate the `meta` service environment variables and change the `PG_META_DB_USER` environment variable from `supabase_admin` to `postgres`: + + ```yml name=docker-compose.yml + meta: + environment: + PG_META_DB_USER: postgres + ``` + + + +Studio uses its own credentials to access Postgres via `postgres-meta`, so this change is only needed for backward compatibility and consistency. + + + +### Step 3: Restart Supabase + +```sh +docker compose down && docker compose up -d +``` + +## Verify roles + +After restarting your services, verify that Supabase Studio is now using the `postgres` role. Run the following query in the Supabase Studio SQL Editor: + +```sql +select current_user; +-- expected result: postgres +``` diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index aa9e9cd0e6c..7bcf1a1c47d 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -37,6 +37,10 @@ services: POSTGRES_HOST: ${POSTGRES_HOST} POSTGRES_DB: ${POSTGRES_DB} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + + # See: https://supabase.com/docs/guides/self-hosting/remove-superuser-access + #POSTGRES_USER_READ_WRITE: postgres + PG_META_CRYPTO_KEY: ${PG_META_CRYPTO_KEY} PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS} PGRST_DB_MAX_ROWS: ${PGRST_DB_MAX_ROWS:-1000} diff --git a/docker/utils/reassign-owner.sh b/docker/utils/reassign-owner.sh new file mode 100644 index 00000000000..d13b1707337 --- /dev/null +++ b/docker/utils/reassign-owner.sh @@ -0,0 +1,153 @@ +#!/bin/sh +# +# Reassign ownership of public schema objects from supabase_admin to postgres. +# +# Context and documentation: +# https://supabase.com/docs/guides/self-hosting/remove-superuser-access +# +# Credits: +# Original version by Inder Singh. +# +# Usage: +# sh utils/reassign-owner.sh +# + +set -e + +if ! docker compose version >/dev/null 2>&1; then + echo "Docker Compose not found." + exit 1 +fi + +# Check Postgres service +db_image_prefix="supabase.postgres:" + +compose_output=$(docker compose ps \ + --format '{{.Image}}\t{{.Service}}\t{{.Status}}' 2>/dev/null | + grep -m1 "^$db_image_prefix" || true) + +if [ -z "$compose_output" ]; then + echo "Postgres container not found. Exiting." + exit 1 +fi + +db_srv_name=$(echo "$compose_output" | cut -f2) +db_srv_status=$(echo "$compose_output" | cut -f3) + +case "$db_srv_status" in + Up*) + ;; + *) + echo "Postgres container status: $db_srv_status" + echo "Exiting." + exit 1 + ;; +esac + +if ! test -t 0; then + echo "" + echo "Running non-interactively. Not reassigning ownership." + exit 0 +fi + +printf "Reassign public schema objects to postgres user? (y/N) " +read -r REPLY +case "$REPLY" in + [Yy]) + ;; + *) + echo "Canceled. Not reassigning ownership." + exit 0 + ;; +esac + +docker compose exec -T "$db_srv_name" psql -v ON_ERROR_STOP=1 -U supabase_admin -d postgres <<'EOF' +\echo 'Current supabase_admin-owned objects in public schema:' +SELECT c.relname, c.relkind, c.relowner::regrole +FROM pg_class c +WHERE c.relnamespace = 'public'::regnamespace +AND c.relowner = 'supabase_admin'::regrole; + +-- Reassign user objects in public schema from supabase_admin to postgres. +-- (Only affects public schema; Supabase-managed schemas stay as-is. +-- Extension-owned objects are skipped.) +DO $$ +DECLARE + rec record; + rel_count int := 0; + fn_count int := 0; + type_count int := 0; +BEGIN + -- Tables, views, sequences, materialized views, partitioned tables + FOR rec IN + SELECT c.relname, c.relkind + FROM pg_class c + WHERE c.relnamespace = 'public'::regnamespace + AND c.relowner = 'supabase_admin'::regrole + AND c.relkind IN ('r', 'v', 'S', 'm', 'p') + AND NOT EXISTS ( + SELECT 1 FROM pg_depend d + WHERE d.classid = 'pg_class'::regclass + AND d.objid = c.oid + AND d.deptype = 'e' + ) + ORDER BY CASE c.relkind + WHEN 'p' THEN 0 -- partitioned parents first; cascades ownership to partitions + WHEN 'm' THEN 1 + WHEN 'r' THEN 2 + WHEN 'v' THEN 3 + WHEN 'S' THEN 4 + END + LOOP + EXECUTE format('ALTER TABLE public.%I OWNER TO postgres', rec.relname); + rel_count := rel_count + 1; + END LOOP; + + -- Functions and procedures + FOR rec IN + SELECT p.oid, p.proname, pg_get_function_identity_arguments(p.oid) AS args + FROM pg_proc p + WHERE p.pronamespace = 'public'::regnamespace + AND p.proowner = 'supabase_admin'::regrole + AND NOT EXISTS ( + SELECT 1 FROM pg_depend d + WHERE d.classid = 'pg_proc'::regclass + AND d.objid = p.oid + AND d.deptype = 'e' + ) + LOOP + EXECUTE format('ALTER ROUTINE public.%I(%s) OWNER TO postgres', rec.proname, rec.args); + fn_count := fn_count + 1; + END LOOP; + + -- Types (excluding array types and table-bound composites) + FOR rec IN + SELECT t.typname + FROM pg_type t + WHERE t.typnamespace = 'public'::regnamespace + AND t.typowner = 'supabase_admin'::regrole + AND t.typrelid = 0 + AND NOT EXISTS ( + SELECT 1 FROM pg_type el + WHERE el.oid = t.typelem + AND el.typarray = t.oid + ) + AND NOT EXISTS ( + SELECT 1 FROM pg_depend d + WHERE d.classid = 'pg_type'::regclass + AND d.objid = t.oid + AND d.deptype = 'e' + ) + LOOP + EXECUTE format('ALTER TYPE public.%I OWNER TO postgres', rec.typname); + type_count := type_count + 1; + END LOOP; + + RAISE NOTICE 'Reassigned % relation(s), % routine(s), % type(s) from supabase_admin to postgres.', + rel_count, fn_count, type_count; +END +$$; +EOF + +echo "" +echo "Done."