diff --git a/apps/docs/content/troubleshooting/revoking-access-to-pg_net-objects-has-no-effect-0bbc16.mdx b/apps/docs/content/troubleshooting/revoking-access-to-pg_net-objects-has-no-effect-0bbc16.mdx new file mode 100644 index 00000000000..a68e79c9716 --- /dev/null +++ b/apps/docs/content/troubleshooting/revoking-access-to-pg_net-objects-has-no-effect-0bbc16.mdx @@ -0,0 +1,114 @@ +--- +title = "Revoking access to pg_net objects has no effect: no privileges could be revoked" +topics = [ "database" ] +keywords = [ "pg_net", "net", "revoke", "grant", "PUBLIC", "privileges", "permissions", "least privilege", "http_request_queue", "_http_response", "http_post", "supabase_admin", "custom role" ] + +[[errors]] +code = "01006" +message = "no privileges could be revoked for \"net\"" + +[[errors]] +code = "01007" +message = "no privileges were granted for \"net\"" +--- + +When you run `revoke` on the `net` schema or its objects as the `postgres` role, the statement completes but access stays the same. Postgres prints a warning instead of an error: + +```text +WARNING: no privileges could be revoked for "net" +``` + +The same warning appears for `http_request_queue`, `_http_response`, and the `net.http_*` functions. A `grant` on these objects prints `no privileges were granted`. The SQL Editor doesn't display warnings, so the statement looks successful there. + +## Check which roles have access + +To list the privileges on the `net` schema and its tables, and the role that granted each one, run the following in the [SQL Editor](/dashboard/project/_/sql/new): + +```sql +select + 'schema net' as object, + case acl.grantee when 0 then 'PUBLIC' else acl.grantee::regrole::text end as grantee, + acl.grantor::regrole as grantor, + acl.privilege_type +from pg_namespace n +cross join lateral aclexplode(n.nspacl) as acl +where n.nspname = 'net' +union all +select + c.relname, + case acl.grantee when 0 then 'PUBLIC' else acl.grantee::regrole::text end, + acl.grantor::regrole, + acl.privilege_type +from pg_class c +cross join lateral aclexplode(c.relacl) as acl +where c.relnamespace = 'net'::regnamespace +order by 1, 2, 4; +``` + +Every row has `supabase_admin` as the grantor. To check what one role can do, replace `my_role` and run: + +```sql +select + has_schema_privilege('my_role', 'net', 'usage') as schema_usage, + has_table_privilege('my_role', 'net.http_request_queue', 'select') as read_queue, + has_function_privilege( + 'my_role', + 'net.http_post(text, jsonb, jsonb, jsonb, integer)', + 'execute' + ) as call_http_post; +``` + +## Why the revoke has no effect + +Supabase creates the pg_net extension as the `supabase_admin` role, so `supabase_admin` owns the `net` schema and everything in it. In Postgres, a role can revoke only the privileges that it granted. For more information, see the Postgres documentation on [`REVOKE`](https://www.postgresql.org/docs/current/sql-revoke.html). + +The extension grants `usage` on the schema and all privileges on its tables and sequences to `PUBLIC`. Its functions are executable by `PUBLIC`, because that's the Postgres default for functions. When the extension is created, Supabase also grants `usage` on `net` to `postgres`, `anon`, `authenticated`, and `service_role`. + +The `postgres` role made none of these grants, so its `revoke` finds nothing to remove. It also holds the privileges without the grant option, so its `grant` to another role also does nothing, and Postgres reports `no privileges were granted`. + +A grant to `PUBLIC` applies to every role, including roles you create later. Postgres has no way to exclude a single role from a `PUBLIC` grant. + +## Which roles can use pg_net + +The following table shows how each kind of role can reach the `net` schema. + +| Role | Can use pg_net | Reason | +| ----------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| `anon`, `authenticated`, `service_role` | No | These roles can't connect to the database directly. The Data API serves only the schemas you expose, and `net` isn't a default. | +| `postgres` and any role you create with `login` | Yes | The role can read and change rows in `net.http_request_queue` and `net._http_response`. The worker sends each row in the queue. | + +On some projects, only specific roles can call `net.http_get` and `net.http_post`. A role you create can still add rows to the queue, and the worker sends them. To check a role, run the `has_function_privilege` query from [Check which roles have access](#check-which-roles-have-access). + +A row in `net.http_request_queue` holds the request's URL, headers, and body until the pg_net worker sends it. A role that can connect to the database can read the `Authorization` header of a queued request. For ways to limit what a queued request exposes, see [Database roles can read request headers queued by pg_net](/docs/guides/troubleshooting/database-roles-can-read-request-headers-queued-by-pg_net-ad6357). + +## Limit pg_net access on your project + +Supabase doesn't support changing the grants on pg_net objects for individual projects, so treat the default grants as a platform constraint. + +Removing the `PUBLIC` grants on the `net` tables breaks pg_net. The `postgres` role reaches those tables only through them, and the pg_net background worker connects as `postgres`. Without the grants, a request function called as `postgres` fails with `permission denied for table http_request_queue`, which also breaks pg_cron jobs that call pg_net. + +To reduce what pg_net exposes: + +- Give the `login` attribute only to roles for services that you trust to send HTTP requests from your database. +- Treat any credential you send in request headers as readable by every role that can connect to your database. +- Keep `net` out of the exposed schemas in your [API settings](/dashboard/project/_/settings/api). +- If your project doesn't use pg_net, drop the extension. Dropping it removes the `net` schema and all of its grants. + + + +Dropping pg_net deletes unsent requests and stored responses, and stops Database Webhooks, which send their requests through pg_net. + + + +To drop the extension, run: + +```sql +drop extension pg_net; +``` + +## When the default grants are applied + +Dropping and re-creating the extension doesn't remove the grants, because `create extension pg_net` applies all of them again. Two other events grant `usage` on `net` to `anon`, `authenticated`, and `service_role` again: + +- A major Postgres version upgrade, which also grants `execute` on `net.http_get` and `net.http_post` to these roles. +- Enabling Database Webhooks.