import net from "node:net"; import tls from "node:tls"; import type { ConnectionOptions } from "node:tls"; export type DatabaseTransportEnvironment = "development" | "production"; /** * The two transport modes this release supports, named after the libpq/PostgreSQL vocabulary an * operator already knows: * * - `verify-full` — TLS is required, the server certificate must chain to a trusted CA, **and** * the certificate identity must match the configured database host. This is the production * default, and the only mode a production deployment may use. * - `disable` — no TLS at all. Available only for the disposable loopback PostgreSQL used by local * development and this repository's integration harness. * * There is deliberately no encryption-only mode (`require`/`verify-ca`) and no * `rejectUnauthorized: false` escape hatch: both authenticate nothing, and an unauthenticated * encrypted channel to an attacker-controlled endpoint is not a security property worth offering. */ export type DatabaseTlsMode = "disable" | "verify-full"; export type DatabaseTransportErrorCode = | "DATABASE_TLS_CONNECTION_STRING_CONFLICT" | "DATABASE_TLS_MODE_INVALID" | "DATABASE_TLS_REQUIRED_IN_PRODUCTION" | "DATABASE_URL_INVALID"; const errorMessages: Readonly> = Object.freeze({ DATABASE_TLS_CONNECTION_STRING_CONFLICT: "The database URL must not carry TLS parameters; configure the database TLS mode explicitly.", DATABASE_TLS_MODE_INVALID: 'The database TLS mode must be exactly "verify-full" or "disable".', DATABASE_TLS_REQUIRED_IN_PRODUCTION: "A production database connection must use certificate-verified TLS.", DATABASE_URL_INVALID: "The database URL must be an absolute postgres:// or postgresql:// URL.", }); /** Never carries the connection string, host, port, database name, credentials, certificate, CA * material, or the offending mode value. */ export class DatabaseTransportError extends Error { readonly code: DatabaseTransportErrorCode; constructor(code: DatabaseTransportErrorCode) { super(errorMessages[code]); this.name = "DatabaseTransportError"; this.code = code; this.stack = `${this.name}: ${this.message}`; Object.freeze(this); } } export interface DatabaseTransportPolicy { readonly mode: DatabaseTlsMode; /** PEM trust material for a private PostgreSQL CA. Optional: when omitted, Node's own trusted CA * set is used. Never logged, never echoed in an error. */ readonly certificateAuthority?: string; } export interface DatabaseTransportPolicyInput { readonly mode?: unknown; readonly certificateAuthority?: unknown; } export interface CreateDatabaseTransportPolicyOptions { /** Defaults to production, so an omitted environment never silently relaxes transport security. */ readonly environment?: DatabaseTransportEnvironment; } /** * Every TLS-related parameter `pg-connection-string@2.14.0` recognizes in a PostgreSQL URL. These * are rejected outright rather than merged, because the pinned driver resolves them in a way that * silently defeats an explicit policy: `pg`'s own `ConnectionParameters` does * `Object.assign({}, config, parse(config.connectionString))` * (`pg/lib/connection-parameters.js`), so a URL parameter **overwrites** the `ssl` object a caller * passed alongside it. Verified directly against a live TLS PostgreSQL instance: a * `?sslmode=disable` URL combined with an explicit certificate-verifying `ssl` object connects in * plaintext. * * The pinned release additionally aliases `prefer`/`require`/`verify-ca` to full verification while * printing a deprecation warning that they will adopt weaker libpq semantics in `pg@9`, so even * a URL that looks safe today is not a durable guarantee. One typed policy is the whole contract. */ const CONNECTION_STRING_TLS_PARAMETERS = Object.freeze([ "ssl", "sslcert", "sslkey", "sslmode", "sslnegotiation", "sslpassword", "sslrootcert", "uselibpqcompat", ]); function parseDatabaseUrl(databaseUrl: string): URL { let url: URL; try { url = new URL(databaseUrl); } catch { throw new DatabaseTransportError("DATABASE_URL_INVALID"); } if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") { throw new DatabaseTransportError("DATABASE_URL_INVALID"); } return url; } /** The host a certificate's identity must match — the exact host the driver will connect to. */ export function databaseHostFromUrl(databaseUrl: string): string { const url = parseDatabaseUrl(databaseUrl); // `URL.hostname` keeps IPv6 literals bracketed; TLS identity checking wants the bare address. const hostname = url.hostname; return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname; } /** Whether the database endpoint is an unambiguous loopback literal. Used only to decide whether * the development plaintext mode is even permissible; it is never a production allowance. */ function isLoopbackDatabaseHost(host: string): boolean { const normalized = host.toLowerCase(); if (normalized === "localhost" || normalized.endsWith(".localhost")) { return true; } if (net.isIPv4(normalized)) { return normalized.startsWith("127."); } if (net.isIPv6(normalized)) { return normalized === "::1" || normalized === "::ffff:127.0.0.1"; } return false; } /** * Validates the typed transport policy. Parsing is deliberately separate from environment policy, * matching the convention the public-signup gate already established: a malformed mode is reported * as malformed in every environment, never relabeled as a production failure. */ export function createDatabaseTransportPolicy( input: DatabaseTransportPolicyInput = {}, options: CreateDatabaseTransportPolicyOptions = {}, ): DatabaseTransportPolicy { const environment = options.environment ?? "production"; let mode: DatabaseTlsMode; if (input.mode === undefined) { // Production fails safe to verified TLS; development keeps the plaintext disposable-PostgreSQL // workflow this repository's harness relies on. mode = environment === "production" ? "verify-full" : "disable"; } else if (input.mode === "verify-full" || input.mode === "disable") { mode = input.mode; } else { // No coercion of booleans, `1`, `"TRUE"`, `" verify-full "`, or any other loose spelling. throw new DatabaseTransportError("DATABASE_TLS_MODE_INVALID"); } if (environment === "production" && mode !== "verify-full") { throw new DatabaseTransportError("DATABASE_TLS_REQUIRED_IN_PRODUCTION"); } if ( input.certificateAuthority !== undefined && (typeof input.certificateAuthority !== "string" || input.certificateAuthority.trim().length === 0) ) { throw new DatabaseTransportError("DATABASE_TLS_MODE_INVALID"); } return Object.freeze({ ...(typeof input.certificateAuthority === "string" ? { certificateAuthority: input.certificateAuthority } : {}), mode, }); } /** * Rejects any database URL carrying its own TLS parameters. Called before a pool or client is ever * constructed, so a URL can never silently downgrade or contradict the validated policy — there is * exactly one place transport security is decided. */ export function assertNoConnectionStringTlsParameters(databaseUrl: string): void { const url = parseDatabaseUrl(databaseUrl); for (const [name] of url.searchParams) { if (CONNECTION_STRING_TLS_PARAMETERS.includes(name.toLowerCase())) { throw new DatabaseTransportError("DATABASE_TLS_CONNECTION_STRING_CONFLICT"); } } } /** * Translates the validated policy into the exact pinned-`pg` options that achieve it, and asserts * the URL cannot override them. * * `verify-full` is **not** simply `ssl: true` or `{ rejectUnauthorized: true }`. Verified directly * against a live TLS PostgreSQL instance: those verify the certificate *chain*, but `pg` only sets * `options.servername` for non-IP hosts (`pg/lib/connection.js`) and passes `tls.connect` a bare * socket carrying no host, so for an IP endpoint Node has no identity to check and falls back to * the fixed name `localhost` — never the address actually dialled. A certificate issued for an * entirely different name is therefore accepted when connecting to an IP address. Supplying an * explicit `checkServerIdentity` pinned to the configured host closes that gap for DNS and IP hosts * alike (proven: a certificate valid only for `localhost` is rejected with * `ERR_TLS_CERT_ALTNAME_INVALID` when reached via `127.0.0.1`). */ export function databaseTlsConnectionOptions( databaseUrl: string, policy: DatabaseTransportPolicy, ): false | ConnectionOptions { assertNoConnectionStringTlsParameters(databaseUrl); if (policy.mode === "disable") { return false; } const host = databaseHostFromUrl(databaseUrl); return { ...(policy.certificateAuthority === undefined ? {} : { ca: policy.certificateAuthority }), // Chain verification. There is no configuration that can set this to `false`. rejectUnauthorized: true, // Identity verification against the host actually being connected to — never the SNI value pg // may or may not have set, and never skipped for an IP endpoint. checkServerIdentity: (_servername: string, certificate: tls.PeerCertificate) => tls.checkServerIdentity(host, certificate), }; } /** * The one production guard every executable database path shares: a production deployment must use * `verify-full`, and its URL must not carry TLS parameters. There is no production plaintext * exception — not for loopback, not for an RFC1918 address, not for a Docker service name. A * production process that genuinely runs beside its database still gets certificate-verified TLS, * which costs nothing and removes an entire class of "it was only internal" misconfiguration. */ export function assertProductionDatabaseTransport( databaseUrl: string, policy: DatabaseTransportPolicy, environment: DatabaseTransportEnvironment, ): void { assertNoConnectionStringTlsParameters(databaseUrl); if (environment !== "production") { return; } if (policy.mode !== "verify-full") { throw new DatabaseTransportError("DATABASE_TLS_REQUIRED_IN_PRODUCTION"); } } /** A fixed, generic description of the effective transport for diagnostics. Never includes the * URL, host, port, database name, credentials, certificate, or CA path. */ export function describeDatabaseTransport(policy: DatabaseTransportPolicy): string { return policy.mode === "verify-full" ? `TLS required with certificate and hostname verification${ policy.certificateAuthority === undefined ? "" : " (explicit private CA supplied)" }.` : "TLS disabled (development/test plaintext)."; } export { isLoopbackDatabaseHost };