feat(bb-data): configurable TLS for external database connections#107
Draft
vishaalmehrishi wants to merge 9 commits into
Draft
feat(bb-data): configurable TLS for external database connections#107vishaalmehrishi wants to merge 9 commits into
vishaalmehrishi wants to merge 9 commits into
Conversation
Add an `ssl` option to the connection-string form of `fromExisting` and
make server-certificate verification the default for external Postgres
connections (Supabase, Neon, RDS, etc.).
Previously the external-DB paths hardcoded `ssl: { rejectUnauthorized: false }`,
so callers had no way to configure TLS verification. `PgClientEngine` already
defaults to `{ rejectUnauthorized: true }`; the runtime now passes the caller's
`ssl` through and defaults to that.
- types: add `ssl?: { rejectUnauthorized?; ca? }` to `ExternalDatabaseRef`
- runtime (index.aws.ts): pass `conn.ssl` through (verify by default)
- local dev (index.mock.ts): honor `connection.ssl`, keep a documented
unverified default for self-signed local databases
- db pull: prompt for the provider CA and write it to a generated, committed
`database.ca.ts` (a public, non-secret cert), refreshed on re-pull and
preserved when not re-supplied; generated `supabase.ts` pins it via a visible
`resolveDbSsl()` (verify-full equivalent), with a `DATABASE_CA_CERT` override
(inline PEM or path) and a visible `rejectUnauthorized: false` fallback
- operational paths (introspect, migrate CLI, migrations) resolve ssl via a
shared `externalDbSsl()` helper; strip `sslmode` so a pinned CA takes effect
- docs: README + generated MIGRATION_GUIDE document CA provisioning
- tests: engine default + explicit ssl, `externalDbSsl`, `generateCaFile`,
generated-wiring assertions; regenerated db-pull-typecheck snapshot
…re-pull test - introspect: strip sslmode via URL.searchParams (not regex); accept an optional CA so `db pull` introspection verifies when one is provided - db pull: extract the database.ca.ts write decision into a tested `resolveCaFileWrite` helper that preserves an existing CA on re-pull - docs: DESIGN.md ssl-default parity row; DECISIONS.md TLS note updated to reflect this change; MIGRATION_GUIDE notes (keep database.ca.ts committed; separate prod project may need its own CA); README example reads the cert contents instead of passing a possibly-undefined env var - changeset: add upgrade/remediation note for direct fromExisting callers - tests: resolveCaFileWrite (incl. preserve-on-re-pull regression) - regenerate API report (adds ssl to ExternalDatabaseRef)
- db pull: the CA-captured confirmation now explains where the cert is written (database.ca.ts), that it's bundled and pins the connection, and that it applies in dev and when deployed - generated wiring: resolveDbSsl() logs the resolved TLS mode on first connect (verifying with pinned CA vs not verified) so success isn't silent - test + regenerated db-pull-typecheck snapshot
soberm
reviewed
Jun 26, 2026
soberm
reviewed
Jun 26, 2026
soberm
left a comment
Contributor
There was a problem hiding this comment.
What happens with an app that used a prior version? Will it keep ssl off?
Contributor
Author
Two cases:
Happy to add the "re-pull to pick up verification" note to the generated migration guide so existing-app users aren't surprised. |
soberm
reviewed
Jun 26, 2026
Resolve the three review threads on PR #107: - PgClientEngineConfig.ssl now reuses the public ExternalSslOptions discriminated union (intersected with minVersion), so the misleading { ca, rejectUnauthorized: false } combination is a compile error for direct engine callers too, not only fromExisting. - externalDbSsl() returns ExternalSslOptions; extract a shared resolveCaPem() helper (inline PEM or file path) so the PEM-vs-path handling can't drift between the operational paths and db pull. - Route db-pull schema introspection through PgClientEngine instead of a raw pg.Pool, so it inherits the TLS 1.2 floor and shared CA handling. - Add an aws-layer test asserting Database._initBase forwards conn.ssl: verify-by-default when ssl is omitted, explicit ssl forwarded. Regenerate API.md for the PgClientEngineConfig.ssl shape change.
The TLS handshake is lazy (it happens on the first query), so a
config-time log only states intent, not what actually happened. Add a
pure tlsConnectionMessage() helper plus a pool 'connect' listener that
logs exactly once, after the handshake succeeds, whether the server
certificate was verified (against a pinned CA or Node's trust store) or
the connection is encrypted-only. This covers every path that builds a
PgClientEngine, including db-pull introspection.
Reword the generated resolveDbSsl() log to match ("will verify ... on
connect") and regenerate the db-pull-typecheck snapshot.
Contributor
Author
Good question — it's a deliberate behavior change, and it splits by how the app connects:
Shipped as a minor with an upgrade/remediation note in the changeset so the break is discoverable. So no — it does not keep ssl off by default anymore; that default was the vulnerability. |
`db pull` threads the captured CA into introspection but not into baseline
generation, so the schema-baseline step connected unverified even when the
operator supplied a CA. Thread `caCert` through to `generateBaseline()`:
- the `server_version_num` check now connects with the pinned CA
(`resolveCaPem` → `{ ca, rejectUnauthorized: true }`) instead of the
env-only `externalDbSsl()` fallback;
- `pg_dump` runs with `PGSSLMODE=verify-full` + `PGSSLROOTCERT` pointing at
the CA written to a short-lived 0600 temp file (removed in a finally).
With no CA, both fall back to the prior encrypted-but-unverified behavior.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds an
ssloption to the connection-string form offromExisting(...)and makes server-certificate verification the default for external PostgreSQL connections (Supabase, Neon, RDS).db pullcaptures the provider's CA so generated apps verify by default — including in the deployed Lambda — with no runtime configuration.Previously the external-database paths hardcoded
ssl: { rejectUnauthorized: false }, so there was no supported way to configure TLS verification for these connections.Approach
The connection's TLS policy is now configurable and verifies by default, with the CA delivered so verification actually works at every layer:
ExternalDatabaseRef's connection-string variant gainsssl?: { rejectUnauthorized?: boolean; ca?: string }.index.aws.ts) — passes the caller'ssslthrough;PgClientEnginealready defaults to{ rejectUnauthorized: true }, so omittingsslverifies.index.mock.ts) — honorsconnection.ssl; keeps an unverified default only for self-signed local databases.db pull— prompts for the provider CA and writes it to a generated, committeddatabase.ca.ts(a public, non-secret certificate — public key + issuer metadata, no private key). The generatedsupabase.tspins it via a visibleresolveDbSsl(). Because it's a bundled JS import (not a runtime file/env read), verification works in the deployed Lambda with no env/SSM/CDK plumbing.DATABASE_CA_CERT(inline PEM or path) overrides it; with no CA the wiring falls back to a visible, editablerejectUnauthorized: false.introspect, migrate CLI, external migrations) — resolvesslvia a sharedexternalDbSsl()helper and stripsslmodefrom the URL so a pinned CA takes effect (nodepgignores a programmaticssl.cawhensslmodeis present).Why a committed cert (not an env var):
.env.*is only loaded on the deploy host, never injected into the Lambda — so an env-only CA would leave the deployed runtime unverified. The CA isn't secret, so committing + bundling it is the simplest mechanism that verifies in production. It's regenerated on eachdb pull(rotation) and preserved when a re-pull doesn't re-supply it (no silent downgrade).What a customer sees
npx bb-data pullnow asks for the CA and explains what it does:Generated
aws-blocks/supabase.ts(the wiring — TLS policy is visible and editable):Generated
aws-blocks/database.ca.ts(regenerated each pull; the customer's actual project CA):How the customer uses it is unchanged — spread
supabaseCrudinto anApiNamespace:On first connect the runtime logs the resolved mode, e.g.
[bb-data] DB TLS: verifying the server certificate against the pinned CA (verify-full equivalent).Behavior change
For hand-written
fromExisting({ connectionString })with nossl, the connection now verifies the server certificate (it previously did not). Private-CA providers (e.g. Supabase) require pinning the CA viassl: { ca }(the certificate contents). Passssl: { rejectUnauthorized: false }to keep the old behavior explicitly.db pull-generated apps are unaffected in default connectivity. Released as aminorbump (see the changeset's upgrade note).Testing
bb-dataunit suite: 336/336 passing (engine default + explicitssl,externalDbSsl,generateCaFile,resolveCaFileWriteincl. the preserve-on-re-pull regression, generated-wiring assertions, updatedtoSessionPortUrl).db-pull-typecheckapptsc --noEmitpasses against the regenerated snapshot (generated wiring imports the committed CA constant).Notes
database.ca.tsholds a certificate only (no private key); the connection string and other secrets continue to flow through SSM, not the repo. The MIGRATION_GUIDE notes keeping it committed and that a separate production project may present its own CA.db pullintrospection resolvessslfrom the captured CA / environment (it runs on the operator host, not the Lambda).