From 12afe399995143a2cc699eabbbd29596f4e64945 Mon Sep 17 00:00:00 2001 From: Illia Basalaiev <44750366+Ellba@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:03:42 +0200 Subject: [PATCH] Docs/troubleshoot pg net privileges (#51275) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? New troubleshooting guides related to the current pg_net and pg_dump extensions' behaviour. ## What is the current behavior? ## What is the new behavior? Preview: - [Revoking access to pg_net objects has no effect: no privileges could be revoked](https://docs-git-docs-troubleshoot-pg-net-privileges-supabase.vercel.app/docs/guides/troubleshooting/revoking-access-to-pg_net-objects-has-no-effect-0bbc16) - [Database roles can read request headers queued by pg_net](https://docs-git-docs-troubleshoot-pg-net-privileges-supabase.vercel.app/docs/guides/troubleshooting/database-roles-can-read-request-headers-queued-by-pg_net-ad6357) - [pg_dump fails with 'query would be affected by row-level security policy'](https://docs-git-docs-troubleshoot-pg-net-privileges-supabase.vercel.app/docs/guides/troubleshooting/pg_dump-fails-with-query-would-be-affected-by-row-level-security-policy-1d5783) - [Custom role inherits privileges that were not explicitly granted](https://docs-git-docs-troubleshoot-pg-net-privileges-supabase.vercel.app/docs/guides/troubleshooting/custom-role-inherits-privileges-that-were-not-explicitly-granted-ddaa1c) --- ...hat-were-not-explicitly-granted-ddaa1c.mdx | 193 ++++++++++++++++++ ...equest-headers-queued-by-pg_net-ad6357.mdx | 38 ++++ ...ed-by-row-level-security-policy-1d5783.mdx | 116 +++++++++++ ...to-pg_net-objects-has-no-effect-0bbc16.mdx | 116 +++++++++++ 4 files changed, 463 insertions(+) create mode 100644 apps/docs/content/troubleshooting/custom-role-inherits-privileges-that-were-not-explicitly-granted-ddaa1c.mdx create mode 100644 apps/docs/content/troubleshooting/database-roles-can-read-request-headers-queued-by-pg_net-ad6357.mdx create mode 100644 apps/docs/content/troubleshooting/pg_dump-fails-with-query-would-be-affected-by-row-level-security-policy-1d5783.mdx create mode 100644 apps/docs/content/troubleshooting/revoking-access-to-pg_net-objects-has-no-effect-0bbc16.mdx diff --git a/apps/docs/content/troubleshooting/custom-role-inherits-privileges-that-were-not-explicitly-granted-ddaa1c.mdx b/apps/docs/content/troubleshooting/custom-role-inherits-privileges-that-were-not-explicitly-granted-ddaa1c.mdx new file mode 100644 index 00000000000..f1e1a8cc694 --- /dev/null +++ b/apps/docs/content/troubleshooting/custom-role-inherits-privileges-that-were-not-explicitly-granted-ddaa1c.mdx @@ -0,0 +1,193 @@ +--- +title = "Custom role inherits privileges that were not explicitly granted" +topics = [ "database" ] +keywords = [ "PUBLIC", "privileges", "permissions", "custom role", "least privilege", "grant", "revoke", "noinherit", "default privileges", "temporary", "lo_create", "template1", "pg_net", "has_table_privilege" ] + +[[errors]] +code = "01006" +message = "no privileges could be revoked for \"template1\"" + +[[errors]] +code = "01006" +message = "no privileges could be revoked for \"lo_create\"" +--- + +A role you create can use objects without an explicit grant. For example, it can read tables in the `net` schema, create temporary tables, or create large objects. Privilege checks such as `has_table_privilege` return `true` for these objects. When you run some revokes as `postgres`, they print `WARNING: no privileges could be revoked` and change nothing. + +The role receives these privileges from `PUBLIC`, which stands for every role in the database, including roles you create later. + +A role you create to limit a service or a person can therefore do more than you intended. When pg_net is enabled, any role that can connect to your database can read the request headers that pg_net queues, including API keys. It can also queue HTTP requests of its own. Any role can use disk space by creating temporary tables and large objects. + +## Find privileges you didn't grant + +To find privileges you didn't grant, list what the role can access and compare it with your own grants. The following queries check schema `usage` and the table and sequence privileges they name. They don't check column-level grants or `execute` on functions. To list the schemas a role can use, replace `app_svc` and run the following in the [SQL Editor](/dashboard/project/_/sql/new): + +```sql +select nspname as schema +from pg_namespace +where + has_schema_privilege('app_svc', oid, 'usage') + and nspname not in ('pg_catalog', 'information_schema') + and nspname not like 'pg\_%' +order by 1; +``` + +To list the tables, views, and sequences the role can read or change in those schemas, run: + +```sql +select + n.nspname as schema, + c.relname as object, + string_agg(p.privilege, ', ' order by p.privilege) as privileges +from pg_class c +join pg_namespace n on n.oid = c.relnamespace +cross join unnest( + array['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'USAGE'] +) as p(privilege) +where c.relkind in ('r', 'p', 'v', 'm', 'f', 'S') + and n.nspname not in ('pg_catalog', 'information_schema') + and has_schema_privilege('app_svc', n.oid, 'usage') + and case + when c.relkind = 'S' then + case + when p.privilege in ('USAGE', 'SELECT', 'UPDATE') + then has_sequence_privilege('app_svc', c.oid, p.privilege) + else false + end + else + case + when p.privilege = 'USAGE' then false + else has_table_privilege('app_svc', c.oid, p.privilege) + end + end +group by 1, 2 +order by 1, 2; +``` + +The results are the role's effective privileges. Access you didn't grant directly can come from `PUBLIC`, from a role it's a member of, or from owning the object. To see who holds privileges on an object, check its access control list: + +```sql +select relacl from pg_class where oid = 'net.http_request_queue'::regclass; +``` + +An entry that starts with `=`, such as `=arwdDxtm/supabase_admin`, is a grant to `PUBLIC`. + +## What every role receives from PUBLIC + +A role holds everything granted to `PUBLIC` in addition to its own grants. The `noinherit` attribute doesn't affect privileges granted to `PUBLIC`, because `PUBLIC` isn't a role membership. + +On a Supabase project, a role you create can use the `public` schema, the system catalogs, and the `net` schema when the pg_net extension is enabled. Other Supabase schemas, such as `auth`, `storage`, `extensions`, `vault`, and `cron`, don't grant `usage` to `PUBLIC`. + +The following table lists what `PUBLIC` holds and whether the `postgres` role can revoke it. + +| Object | What `PUBLIC` holds | Can `postgres` revoke it | +| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------ | +| `postgres` database | `connect` and `temporary` | Yes | +| `template1` database | `connect`. The database holds none of your data. | No | +| `public` schema | `usage`, and `execute` on functions you create there | Yes | +| `net` schema | `usage`, all privileges on `net.http_request_queue` and `net._http_response`, and on most projects `execute` on the `net.http_*` functions | No | +| Functions in `cron` and `extensions` | `execute`, which a role can't use without `usage` on the schema | Not needed | +| Large-object functions such as `lo_create` | `execute`. A role can create large objects in the database. | No | +| System catalogs such as `pg_class` and `pg_proc` | Read access to the names and definitions of database objects | No | + +The `postgres` role owns the `postgres` database, and through it the `public` schema, so it can revoke the `PUBLIC` grants on those. Supabase roles own the other objects, so a revoke on them prints the `no privileges could be revoked` warning. For the pg_net grants, see [Revoking access to pg_net objects has no effect](/docs/guides/troubleshooting/revoking-access-to-pg_net-objects-has-no-effect-0bbc16). + +Extensions you enable can grant more privileges to `PUBLIC`. After you enable an extension, check the role again. + +## Revoke PUBLIC privileges on objects you own + +You can revoke the `PUBLIC` grants on the `public` schema, on the `postgres` database, and on functions you create. + + + +Revoking `connect` on the `postgres` database from `PUBLIC` disconnects the Data API, Auth, Storage, and other Supabase services, because their roles connect through that grant. Leave `connect` granted to `PUBLIC`. + + + +### Functions you create + +To stop `PUBLIC` from running functions that `postgres` creates after the change, update the default privileges without naming a schema: + +```sql +alter default privileges for role postgres + revoke execute on functions from public; +``` + +A default privilege set with `in schema` can't remove the `PUBLIC` default for functions, because Postgres adds schema defaults to the global ones. + +The Supabase defaults also grant `execute` to `anon`, `authenticated`, and `service_role` on functions that `postgres` creates in `public`. Those roles keep their access. If you changed those defaults, or create functions as another role, grant `execute` to the API roles that need it. + +To check the default privileges for functions, run: + +```sql +select defaclrole::regrole as owner, defaclnamespace::regnamespace as schema, defaclacl +from pg_default_acl +where defaclobjtype = 'f'; +``` + +Functions in other schemas don't get those grants. If a Row Level Security policy calls a helper function outside `public`, grant `execute` on it to the roles the policy applies to: + +```sql +grant execute on function private.is_admin() to authenticated; +``` + +To revoke `execute` from `PUBLIC` on a function that already exists, run: + +```sql +revoke execute on function public.my_function() from public; +``` + +### The public schema + +To stop roles you create from using the `public` schema without a grant, run: + +```sql +revoke usage on schema public from public; +``` + +The `postgres`, `anon`, `authenticated`, and `service_role` roles keep their own `usage` grants, so the Data API keeps working. Other roles lose access to the schema unless they have their own grant, so grant `usage` to each role you create that needs it. Test the change on a [preview branch](/docs/guides/deployment/branching) before you apply it to production. + +### Temporary tables + +To allow temporary tables only for roles that you grant `temporary` to, run: + +```sql +revoke temporary on database postgres from public; +``` + +Grant `temporary` back to each role that needs it. If your Data API functions create temporary tables, also grant it to `authenticated`. Test this change on a preview branch too. + +### Confirm the change + +To confirm that the role lost the privileges you revoked, replace `app_svc` and `public.my_function()` and run: + +```sql +select + has_schema_privilege('app_svc', 'public', 'usage') as can_use_public_schema, + has_database_privilege('app_svc', 'postgres', 'temporary') as can_create_temp_tables, + has_function_privilege('app_svc', 'public.my_function()', 'execute') as can_run_my_function; +``` + +Each column returns `false` unless the role still receives the privilege from its own grant or from a role it's a member of. + +## Grant only what a service needs + +Give each service its own role, and grant only the schemas and tables it uses. The following example creates a role that can read and add orders in an `app` schema: + +```sql +create role app_svc login password ''; +grant usage on schema app to app_svc; +grant select, insert on app.orders to app_svc; +grant usage on sequence app.orders_id_seq to app_svc; +``` + +A role you create is subject to Row Level Security on tables it doesn't own, unless you give it the `bypassrls` attribute. A table's owner bypasses its policies unless you run `alter table force row level security`. + +Don't grant the role membership in `postgres` or in other Supabase roles, because a member role inherits all of their privileges. For more information about roles, see [Postgres Roles](/docs/guides/database/postgres/roles). + +## Settings that don't restrict a role + +Some settings look like restrictions, but a session can change them. Use grants to restrict a role. + +- `default_transaction_read_only`: a session can run `set default_transaction_read_only = off` and then write. Use it to prevent accidental writes only. +- `search_path`: a session can set its own search path or use schema-qualified names. diff --git a/apps/docs/content/troubleshooting/database-roles-can-read-request-headers-queued-by-pg_net-ad6357.mdx b/apps/docs/content/troubleshooting/database-roles-can-read-request-headers-queued-by-pg_net-ad6357.mdx new file mode 100644 index 00000000000..62a3c37ee0b --- /dev/null +++ b/apps/docs/content/troubleshooting/database-roles-can-read-request-headers-queued-by-pg_net-ad6357.mdx @@ -0,0 +1,38 @@ +--- +title = "Database roles can read request headers queued by pg_net" +topics = [ "database" ] +keywords = [ "pg_net", "net", "http_request_queue", "_http_response", "http_post", "headers", "Authorization", "bearer token", "secret key", "service_role", "Vault", "credentials", "Row Level Security" ] +--- + +When you call `net.http_get`, `net.http_post`, or `net.http_delete`, pg_net stores the request as a row in `net.http_request_queue`. The row holds the URL, headers, and body until the pg_net worker sends the request. Any role that can use pg_net can read the row while it waits, including an `Authorization` or `apikey` header. For which roles can use pg_net, see [Revoking access to pg_net objects has no effect](/docs/guides/troubleshooting/revoking-access-to-pg_net-objects-has-no-effect-0bbc16#which-roles-can-use-pg_net). + +## Check what your requests expose + +To see the requests waiting to be sent, run the following in the [SQL Editor](/dashboard/project/_/sql/new): + +```sql +select id, url, headers +from net.http_request_queue +order by id; +``` + +The worker deletes a row from the queue when it picks up the request, so most rows exist for a short time. Rows stay in the queue for longer when the worker falls behind or stops. + +To see the stored responses, run: + +```sql +select id, status_code, headers, content, created +from net._http_response +order by created desc +limit 20; +``` + +`net._http_response` keeps the status code, response headers, and response body for at least the time set by `pg_net.ttl`, which is 6 hours by default. The worker deletes expired responses while it processes requests, so responses can stay longer when no requests are being sent. It doesn't store request headers. Check the response bodies too, because an API can return sensitive data of its own. + +## Credentials in request headers + +Treat any credential you send through pg_net as readable by every role that can connect to your database. Reading a credential from [Vault](/docs/guides/database/vault) keeps it encrypted at rest, but the decrypted value is written to the `headers` column like any other header. + +## Row Level Security on the queue + +You can't add Row Level Security policies to `net.http_request_queue` or `net._http_response`. The `supabase_admin` role owns both tables, and only a table's owner can enable Row Level Security on it. Supabase doesn't support changing the grants or policies on these tables for individual projects, so treat them as a platform constraint. diff --git a/apps/docs/content/troubleshooting/pg_dump-fails-with-query-would-be-affected-by-row-level-security-policy-1d5783.mdx b/apps/docs/content/troubleshooting/pg_dump-fails-with-query-would-be-affected-by-row-level-security-policy-1d5783.mdx new file mode 100644 index 00000000000..ebe1f9b77f1 --- /dev/null +++ b/apps/docs/content/troubleshooting/pg_dump-fails-with-query-would-be-affected-by-row-level-security-policy-1d5783.mdx @@ -0,0 +1,116 @@ +--- +title = "pg_dump fails with 'query would be affected by row-level security policy'" +topics = [ "database" ] +keywords = [ "pg_dump", "supabase db dump", "backup", "export", "read-only role", "pg_read_all_data", "bypassrls", "Row Level Security", "RLS", "backup role", "read replica" ] + +[[errors]] +code = "42501" +message = "query would be affected by row-level security policy for table" + +[[errors]] +code = "42501" +message = "permission denied to set role \"postgres\"" + +[[errors]] +code = "42501" +message = "permission denied for large object" +--- + +When you run `pg_dump` as a role you created, the export stops at the first table that uses Row Level Security: + +```text +pg_dump: error: query would be affected by row-level security policy for table "orders" +``` + +`pg_dump` turns off row security for its session, so it exports every row of a table or none. A role without the `bypassrls` attribute can't read every row, so the export fails. For more information, see the Postgres documentation on [`pg_dump`](https://www.postgresql.org/docs/current/app-pgdump.html). + +## Create a backup role + +Create a dedicated role that can read every table without the `postgres` password. Run the following in the [SQL Editor](/dashboard/project/_/sql/new): + +```sql +create role backup_reader login password '' bypassrls; +grant pg_read_all_data to backup_reader; +alter role backup_reader set default_transaction_read_only = on; +``` + +Each statement does one job: + +- `bypassrls` lets `pg_dump` read every row. +- `pg_read_all_data` is a predefined Postgres role. It lets `backup_reader` read every table, view, and sequence in every schema, including `auth` and `storage`. +- `default_transaction_read_only` makes the role's sessions read-only by default, which prevents accidental writes. + +`pg_read_all_data` doesn't cover large objects. If your database has large objects, a full export fails with `permission denied for large object`. + +To export without large objects, add `--no-large-objects` to the `pg_dump` command. In `pg_dump` 15 and earlier, the option is `--no-blobs`. + +To include large objects, grant `select` on each one to `backup_reader`. Only a large object's owner, or a role that holds `select` on it with the grant option, can grant access to it. + +1. List the roles that own large objects: + + ```sql + select distinct lomowner::regrole as owner from pg_largeobject_metadata; + ``` + +2. Run the following block once as each of those roles: + + ```sql + do $$ + declare + r record; + begin + for r in + select oid from pg_largeobject_metadata + where lomowner = current_user::regrole + loop + execute format('grant select on large object %s to backup_reader', r.oid); + end loop; + end + $$; + ``` + +To give another role you created the same access, run `alter role bypassrls;` and `grant pg_read_all_data to ;`. + +## Run the export + +1. Open the [**Connect** panel](/dashboard/project/_?showConnect=true) and copy the session pooler or direct connection string. +2. Change the username in the connection string, and remove the password from it. For the shared pooler, change `postgres.` to `backup_reader.`. For a direct connection, change `postgres` to `backup_reader`. +3. Run `pg_dump` with the connection string. Use `pg_dump` from the same major Postgres version as your project, or a later one. + + ```bash + pg_dump "postgresql://backup_reader.@:5432/postgres" \ + --password \ + --file backup.sql + ``` + + `--password` prompts for the password. For a scheduled export, leave out `--password` and store the password in a password file. For the file format, see the Postgres documentation on [the password file](https://www.postgresql.org/docs/current/libpq-pgpass.html). + +To export specific schemas, add a `--schema` option for each one, for example `--schema=public --schema=auth --schema=storage`. An export with `--schema` leaves out large objects. To include them, also add `--large-objects`, which is `--blobs` in `pg_dump` 15 and earlier. + +Run `pg_dump` directly rather than `supabase db dump`. The Supabase CLI command switches to the `postgres` role during the export, so it fails for `backup_reader` with `permission denied to set role "postgres"`. + +The export contains [Vault](/docs/guides/database/vault) secrets in encrypted form only, because `backup_reader` can't decrypt them. The export doesn't include role passwords. + +If your project has a [Read Replica](/docs/guides/platform/read-replicas), you can export from the replica to keep the load off your Primary database. To find the replica's connection string, set **Source** to the replica in the **Connect** panel. A Read Replica is a copy of the Primary, including its roles, so `backup_reader` can also connect to the Primary with the same password. + +## What the backup role can still do + +A session as `backup_reader` can still write in these ways: + +- The session can run `set default_transaction_read_only = off`, because the setting is a default. +- If the pg_net extension is enabled, the session can add rows to `net.http_request_queue`, which pg_net sends as HTTP requests. The role receives this privilege from `PUBLIC`. +- The session can create large objects with functions such as `lo_create`, which `PUBLIC` can run. + +For the full list of privileges every role receives, see [Custom role inherits privileges that were not explicitly granted](/docs/guides/troubleshooting/custom-role-inherits-privileges-that-were-not-explicitly-granted-ddaa1c#what-every-role-receives-from-public). + +## Rotate the password + +Store the role's password only in the system that runs the export. If that system is compromised, change the password: + +```sql +alter role backup_reader password ''; +``` + +Connections through the shared pooler can fail for about 15 seconds after the change, until the pooler picks up the new password. + +Supabase backs up Pro, Team, and Enterprise Plan projects daily without any credentials. For more information about those backups, see [Database Backups](/docs/guides/platform/backups). 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..acfb88d3a08 --- /dev/null +++ b/apps/docs/content/troubleshooting/revoking-access-to-pg_net-objects-has-no-effect-0bbc16.mdx @@ -0,0 +1,116 @@ +--- +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`. On most projects, its functions are also executable by `PUBLIC`, which is the Postgres default for functions. For the projects where they aren't, see [Which roles can use pg_net](#which-roles-can-use-pg_net). 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 holds the privileges without the grant option, so its `grant` to another role does nothing either, 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, by default | These roles can't connect to the database directly, and the Data API doesn't expose `net` by 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 `postgres`, `anon`, `authenticated`, `service_role`, and `supabase_functions_admin` can call `net.http_get` and `net.http_post`. For the events that revoke `execute` from `PUBLIC`, see [When the default grants are applied](#when-the-default-grants-are-applied). 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. To check 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, because the `postgres` role reaches those tables only through them. 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 you expose it, `anon` and `authenticated` can read `net.http_request_queue` through the `PUBLIC` grants, including queued credentials. +- 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. Before you drop it, check that `net.http_request_queue` is empty and that your project has no Database Webhooks. + + + +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. +- Turning on Database Webhooks. + +A major Postgres version upgrade also revokes `execute` on `net.http_get` and `net.http_post` from `PUBLIC`, and grants it to `postgres`, `anon`, `authenticated`, `service_role`, and `supabase_functions_admin`. The same happens when pg_net 0.11 or earlier is enabled on a project.