docs(supabase): harden operational safety guidance (#57)

* docs(supabase): harden perimeter and recovery guidance

* docs(supabase): lock in operational safety patterns
This commit is contained in:
Magnus Hedemark
2026-07-16 10:30:03 -04:00
committed by GitHub
parent fe252b3df6
commit 089685cef2
5 changed files with 115 additions and 1 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ Treat Supabase as a Postgres-centered system with multiple independently version
| Need | Read first |
|---|---|
| Understand services, trust boundaries, keys, and environment differences | [architecture and boundaries](references/architecture-and-boundaries.md) |
| Install/use the CLI or establish a reproducible local workflow | [local development and CLI](references/local-development-and-cli.md) |
| Install/use the CLI, establish a local workflow, or operate managed Functions, secrets, branches, SSL enforcement, CIDR restrictions, and network bans | [local development and CLI](references/local-development-and-cli.md) |
| Create schemas, migrations, seed data, RLS policies, tests, and generated types | [database development and testing](references/database-development-and-testing.md) |
| Build with Auth, REST, Realtime, Storage, and Edge Functions | [application services](references/application-services.md) |
| Deploy or harden the official Docker stack | [self-hosting deployment](references/self-hosting-deployment.md) |
+24
View File
@@ -9,6 +9,7 @@
"Does not make an untracked Dashboard-only or live-database-only schema change.",
"Includes both USING and WITH CHECK reasoning where writes can create or change ownership.",
"Tests cross-user denial with authenticated sessions and does not use service role as RLS proof.",
"Establishes ordinary schema/table grants before using direct pgTAP exception and zero-row assertions, then re-reads as the owner to prove the invariant.",
"Runs or requires db reset, database tests, and type regeneration before completion."
]
},
@@ -66,6 +67,29 @@
"Accounts for migrations, Functions, configuration, and branch-specific secrets.",
"Verifies branch health and migration results instead of treating branch creation as deployment success."
]
},
{
"id": "managed-perimeter-lockout",
"prompt": "A managed Supabase CIDR change locked out an operator, and database SSL enforcement must be rolled back safely. Diagnose and recover without guessing.",
"expected_output": "The agent reads current SSL, network restriction, and ban state; distinguishes the blocking layer; preserves alternate access; applies only exact rollback changes; reads state back; and tests real allowed and denied TLS connections.",
"assertions": [
"Routes managed perimeter work from SKILL.md to references/local-development-and-cli.md before proposing commands.",
"Uses ssl-enforcement get/update with the appropriate enable or disable flag.",
"Uses network-restrictions get/update and preserves replacement versus append semantics.",
"Uses network-bans get before removing only a proven IP ban.",
"Does not invent --experimental, default to --bypass-cidr-checks, or declare success without read-back and real connection tests."
]
},
{
"id": "recovered-key-material",
"prompt": "Recover a lost self-hosted Supabase host from a backed-up Docker directory, db-config volume, and environment record. Preserve access and tell me when first boot is proven.",
"expected_output": "The agent restores db-config before data, verifies pgsodium key presence without disclosing content or a fingerprint, preserves recovered signing/API material, treats regeneration as rotation, and uses evidence-based first-boot gates.",
"assertions": [
"Uses a non-printing presence check such as test -s and reports only file metadata; never cat, hexdump, or content hash.",
"Preserves recovered JWT, signing, API, and database credentials rather than regenerating during recovery.",
"Distinguishes genuinely new-install generation from explicit post-recovery rotation.",
"Uses config, bounded logs, observed migration progress, health convergence, and smoke checks without invented durations or universal restart claims."
]
}
]
}
@@ -28,6 +28,14 @@ A recoverable self-hosted deployment includes more than Postgres:
Do not call a backup complete until it restores into a separate target and representative Auth, REST/RLS, Storage, Realtime, and Functions flows pass.
## Recovery-key gate
Restore the backed-up `db-config` volume before database data so the original `pgsodium_root.key` is available when encrypted values are read. Confirm presence without disclosing content or a reusable fingerprint: run a non-printing `test -s` inside the database container and inspect only file type, ownership, and permissions. Never use `cat`, `hexdump`, or a content hash in reports.
Preserve the recovered `.env` JWT secret, asymmetric signing material, publishable/anonymous keys, secret/service-role keys, and database password during recovery. Running key generators against a recovered deployment is rotation, not restoration, and can invalidate sessions or clients. Use `generate-keys.sh` followed by `add-new-auth-keys.sh` only for a genuinely new manual installation whose keys do not exist; treat later regeneration as an explicit rotation event.
For first boot, validate resolved configuration, inspect service state and bounded logs, observe migrations advancing, wait for health convergence, and run the official plus external-boundary smoke checks. A nonzero first start is inconclusive while logs show forward progress. Do not invent polling intervals, expected durations, or a universal restart rule.
## Logical dump and restore
For Supabase-aware migration/restore, prefer `supabase db dump` over raw `pg_dump`; it filters managed internals and reserved roles:
@@ -94,6 +94,65 @@ RLS suites should cover:
Use `begin`/`rollback`, set the local role, and set request JWT claims deliberately. Require negative assertions, not only happy paths.
### Executable negative-test pattern
Adapt table and column names to the migration under test, but preserve the privilege and assertion semantics:
```sql
begin;
select plan(5);
grant usage on schema public to authenticated;
grant select, insert, update, delete on public.documents to authenticated;
set local role authenticated;
select set_config('request.jwt.claims', '{"sub":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","role":"authenticated"}', true);
select throws_ok(
$$insert into public.documents (id, owner_id, body)
values ('10000000-0000-0000-0000-000000000001',
'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'foreign')$$,
'42501'
);
select is(
(with changed as (
update public.documents set owner_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
where id = '20000000-0000-0000-0000-000000000002' returning 1
) select count(*) from changed),
0::bigint, 'USING hides cross-owner update'
);
select set_config('request.jwt.claims', '{"sub":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","role":"authenticated"}', true);
select is(
(select owner_id from public.documents
where id = '20000000-0000-0000-0000-000000000002'),
'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::uuid,
'owner re-read proves ownership unchanged'
);
select set_config('request.jwt.claims', '{"sub":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","role":"authenticated"}', true);
select is(
(with deleted as (
delete from public.documents
where id = '20000000-0000-0000-0000-000000000002' returning 1
) select count(*) from deleted),
0::bigint, 'USING hides cross-owner delete'
);
select set_config('request.jwt.claims', '{"sub":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","role":"authenticated"}', true);
select is(
(select count(*) from public.documents
where id = '20000000-0000-0000-0000-000000000002'),
1::bigint, 'owner re-read proves row remains'
);
select * from finish();
rollback;
```
The direct SQL passed to `throws_ok` proves a `WITH CHECK` exception only after the ordinary grants exist. The data-modifying CTEs prove the zero-row behavior of `USING`; owner re-reads prove the invariant. Lock the expected SQLSTATE or error text to the supported Postgres/Supabase release rather than guessing across versions.
## Application-level tests
Test the client/gateway path in addition to pgTAP. Use unique users and object names so tests can run independently; clean up what they create. Test the publishable key plus user sessions. Keep the secret key in server-only setup/cleanup code and do not use it for behavior under test.
@@ -119,6 +119,29 @@ supabase config push
Use `functions serve` and application tests before deployment. Keep local and platform secrets out of source; `config.toml` should use `env(NAME)` references where supported. `config push`, `functions deploy`, and `secrets set` mutate the linked managed project, so verify the project reference and intended environment first. Read current CLI help and Functions documentation before assuming a flag applies to self-hosted Docker; the platform deploy command and mounted self-hosted Functions workflow are different.
## Managed database perimeter
Treat command discovery, eligibility, mutation, and verification as separate gates. Capture current state first:
```sh
supabase --version
supabase ssl-enforcement get --project-ref PROJECT_REF
supabase network-restrictions get --project-ref PROJECT_REF
supabase network-bans get --project-ref PROJECT_REF
```
Before mutation, confirm the project and account permissions, record the current outputs as rollback evidence, preserve an alternate administrative and database access path, and inspect current command help. Help proves syntax, not plan eligibility or safe operational effect.
```sh
supabase ssl-enforcement update --project-ref PROJECT_REF --enable-db-ssl-enforcement
supabase ssl-enforcement update --project-ref PROJECT_REF --disable-db-ssl-enforcement
supabase network-restrictions update --project-ref PROJECT_REF \
--db-allow-cidr CIDR_1 --db-allow-cidr CIDR_2
supabase network-bans remove --project-ref PROJECT_REF --db-unban-ip IP_ADDRESS
```
`network-restrictions update` replaces the current CIDR set unless `--append` is chosen deliberately. Do not default to `--bypass-cidr-checks`. Remove only an IP proven by `network-bans get` to be banned; distinguish bans from CIDR restrictions, TLS, credentials, and routing failures first. After a change, repeat the corresponding `get`, then test a real TLS database connection from an allowed source and a denied source. Restore the recorded SSL and restriction state if access or application checks fail. Do not invent `--experimental` requirements or rollback timers.
## Managed branches and previews
Managed-platform branches are isolated preview or persistent environments, not Git branches and not a self-hosted Compose feature. Confirm current availability, pricing, and beta status before designing a workflow around them.