Skip to content

fix(cli): pin the Supabase CA on every linked database connection - #7518

Merged
jgoux merged 7 commits into
developfrom
fix/linked-db-pin-supabase-ca
Nov 6, 2026
Merged

jgoux merged 7 commits into
developfrom
fix/linked-db-pin-supabase-ca

Conversation

@jgoux

@jgoux jgoux commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Only gen types checked hosted database servers against the bundled Supabase root CA. Every other command that connects to a linked project used sslmode unset, which means no certificate verification. That covers db push/pull/dump/diff/reset/lint/advisors/query, migration * and inspect *.

The shared linked resolver now pins the CA. It uses sslmode=require plus the inline bundle, which the driver verifies as verify-ca.

Where the pin applies:

  • the direct db.<ref> host;
  • the pooler URL (saved or from the Management API);
  • the temp-role probe, which previously connected before any pin;
  • the container pooler fallback;
  • the bootstrap push when it falls back to the direct host.

When it applies:

  • Profiles: only supabase, supabase-staging and snap, and only on that profile's own direct-host or pooler domains. supabase-local and YAML profiles are never pinned.
  • Explicit settings win: an explicit sslmode from any source (DSN, pg_service.conf, PGSSLMODE) is used as is, including disable and verify-full. A root CA set without sslmode (PGSSLROOTCERT, DSN or service file) replaces the bundled CA and keeps require, so the chain is verified against it. gen types --db-url honors the DSN as is.
  • Not covered: tools the CLI spawns that build their own connection, such as pg_prove in test db and the pgAdmin and migra fallbacks in db diff.

Behavior changes:

  • Linked commands on the built-in profiles now verify the server certificate chain. They already used TLS but accepted any certificate, so a man-in-the-middle could read the database password; gen types already verified it. A certificate that does not chain to the Supabase CA, for example from a TLS-terminating proxy in front of Postgres, now fails. Set PGSSLROOTCERT to a CA bundle that trusts it, or PGSSLMODE=require to skip verification as before.
  • The temp-role probe stops at once on a rejected certificate (suggesting the settings above instead of SUPABASE_DB_PASSWORD) or when network restrictions reject the caller's IP (Address not in tenant allow_list, with a link to the dashboard's database settings), instead of retrying for about 2.5 minutes. Auth and dial failures keep retrying, since a network ban shows up as a dial failure.
  • gen types on a linked project or preview branch no longer overrides an explicit PGSSLMODE/PGSSLROOTCERT.
  • db dump passes PGSSLMODE=require to pg_dump, without the inline CA, so the dump requires TLS but does not verify the chain. This is documented in its SIDE_EFFECTS.md.

Cleanup: the CA templates move to command-internal/, and migra's duplicate PG_DELTA_CA_BUNDLE now uses the same bundle.

The pooler certificates on aws-0-us-east-1.pooler.supabase.com, aws-1-eu-west-1.pooler.supabase.com and aws-0-us-east-1.pooler.snapcloud.co chain to Supabase Root 2021 CA in the bundle (openssl s_client -starttls postgres). For direct hosts, gen types already pinned them unconditionally.

Verification

  • bun --bun vitest run --project integration on db-config.integration.test.ts, gen/types/types.integration.test.ts and db/diff: 417 passed. New tests cover:
    • the direct host on each pinned profile;
    • the pooler;
    • the temp-role probe;
    • explicit verify-full, disable, PGSSLROOTCERT, the service file and the pooler URL sslmode;
    • supabase-local, a YAML profile and a foreign host;
    • the temp-role probe failing fast on a rejected certificate while auth failures and bans still retry;
    • host matching across case, a trailing dot and lookalike domains.
  • After the review fixes: the same integration files plus bootstrap: 471 passed; supabase-tls and connect-errors unit tests: 69 passed. The bootstrap direct-host fallback test fails without the pin.
  • pnpm run types:check, oxfmt and oxlint are clean.

Only gen types verified hosted servers against the bundled Supabase root CA.
The shared linked resolver now pins it on the direct host and every pooler
path, including the temp-role probe, for the supabase, supabase-staging and
snap profiles. An explicit sslmode or sslrootcert from any source wins.
@jgoux
jgoux requested a review from a team as a code owner November 5, 2026 15:55
@jgoux
jgoux requested a review from kanadgupta November 5, 2026 15:55
@jgoux jgoux self-assigned this Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

CLI benchmark (Lightweight Commands): no changes found

Commit c84b864 against develop at f530933 · 10 runs each · report only, never blocks merging

Command Workload Before After Change
supabase --version version 382 ms 351 ms no real change (-8%, within normal variation)
supabase --help help 348 ms 334 ms no real change
supabase init init 285 ms 272 ms no real change
supabase migration new bench migration-new 254 ms 237 ms no real change (-7%, within normal variation)
supabase gen types --lang typescript --project-id replayfixtureprojref gen-types 254 ms 253 ms no real change
supabase projects list projects-list 226 ms 224 ms no real change
supabase functions list --project-ref replayfixtureprojref functions-list 220 ms 218 ms no real change

API calls: unchanged for all 7 commands.

Binary size: 134.7 MB, down 8.0 KB from 134.7 MB.

Details: timing ranges, API calls per command, slowest phases

Timing ranges

Command Workload Before range After range Change Verdict
supabase --version version 320-477 ms 314-455 ms -31 ms (-8%) within noise
supabase --help help 286-406 ms 276-512 ms -13 ms (-4%) within noise
supabase init init 251-378 ms 247-360 ms -13 ms (-5%) within noise
supabase migration new bench migration-new 218-330 ms 222-316 ms -17 ms (-7%) within noise
supabase gen types --lang typescript --project-id replayfixtureprojref gen-types 240-291 ms 229-271 ms -1 ms (0%) within noise
supabase projects list projects-list 205-240 ms 212-243 ms -1 ms (-1%) within noise
supabase functions list --project-ref replayfixtureprojref functions-list 202-279 ms 210-283 ms -3 ms (-1%) within noise

API calls per command

Command Workload Before After Endpoint changes
supabase --version version 0 0
supabase --help help 0 0
supabase init init 0 0
supabase migration new bench migration-new 0 0
supabase gen types --lang typescript --project-id replayfixtureprojref gen-types 2 2
supabase projects list projects-list 1 1
supabase functions list --project-ref replayfixtureprojref functions-list 2 2

Slowest phases

No command moved beyond noise, so there are no phase deltas.

Workflow run

Comment /bench to run the lightweight benchmark (this comment) again on the latest commit, or /bench full to also run the integration-heavy benchmark, which posts in its own comment.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI Review

Confirmed both Claude findings after reading the checked-out code and trusted compatibility contracts. Corrected the breaking-change finding's location and scope: non-TypeScript project generation changes its TLS environment handling; preview direct connections still ignore PG* settings. Raised the certificate-verification regression to critical under the supplied severity definitions. Codex reported no findings. No fundamental conflict with pending next-branch work was found. Tests were not run.

Findings

Severity Location Category Sources Claim
🔴 CRITICAL apps/cli/src/commands/gen/types/types.handler.ts:695 security claude gen types --db-url silently loses certificate verification when a YAML profile with a custom name points to Supabase hosts. Previously, matching the profile's project_host or pooler_host caused CA pinning. The new helper skips names outside its built-in allowlist, leaving TLS unverified and permitting server impersonation.
🟠 MAJOR apps/cli/src/command-internal/supabase-tls.ts:37 breaking-change claude Released connection behavior changes: linked commands using built-in hosted profiles now default to CA verification, rejecting certificate chains that previously worked with unverified TLS. Non-TypeScript gen types against a project ref now honors PGSSLMODE/PGSSLROOTCERT instead of overriding them with its pinned mode and CA. For example, PGSSLMODE=disable now selects plaintext and fails against an SSL-enforced project. Please use a fix(cli)!: title, add a BREAKING CHANGE: line to the PR description, and retarget the PR from develop to next.

Stats

Claude findings: 2 · Codex findings: 0 · Confirmed: 2 · Refuted: 0 · Uncertain: 0


Models: claude-opus-5-5 + gpt-6.1-sol · Trigger: auto · Workflow run

This review runs once per PR. A maintainer can request another with a /ai-review comment.

- stop the temp-role retries at once on a rejected server certificate
- add a CA-verification hint (PGSSLROOTCERT / PGSSLMODE=require), shown on the temp-role error
- keep retrying auth and dial failures so propagation and ban detection are unchanged
- normalize host case and trailing dot before matching direct hosts in pinSupabaseTls
- document the built-in-profile restriction and the preview-branch pooler PG* handling
- wrap an over-long JSDoc line in refuse-invalid-db-hosts
jgoux added 2 commits November 5, 2026 19:06
… the IP

The temp-role probe retried for about 2.5 minutes when the pooler rejected the
caller's IP (Address not in tenant allow_list), which no retry can fix. It now
fails on the first attempt with the network-restrictions suggestion.
…supabase-ca

# Conflicts:
#	apps/cli/src/commands/gen/types/SIDE_EFFECTS.md

@7ttp 7ttp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was poking around this one so figured i'd jump in, tested it and it works nicely! :D

left a few smol comments inline:

jgoux added 3 commits November 6, 2026 09:33
A root CA set without sslmode on a hosted host now keeps sslmode=require,
so the driver verifies the chain against it instead of skipping
verification. The bootstrap IPv6 fallback connection gets the pin too.

The temp-role probe stops retrying on a hostname mismatch
(ERR_TLS_CERT_ALTNAME_INVALID), no longer treats OUT_OF_MEM as a
certificate rejection, and its network restrictions hint links the
dashboard's database settings.
…supabase-ca

# Conflicts:
#	apps/cli/src/commands/gen/types/SIDE_EFFECTS.md
#	apps/cli/src/commands/gen/types/types.handler.ts
@jgoux
jgoux added this pull request to the merge queue Nov 6, 2026
Merged via the queue into develop with commit 948d4d4 Nov 6, 2026
54 checks passed
@jgoux
jgoux deleted the fix/linked-db-pin-supabase-ca branch November 6, 2026 09:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants