From 605c0e00522d5a1928c769a1e011b59b26dada9a Mon Sep 17 00:00:00 2001 From: Leon Serfaty <80597822+silkoserfo@users.noreply.github.com> Date: Wed, 26 Aug 2026 11:19:14 -0400 Subject: [PATCH] Allow plaintext Postgres in production for private-network hosts only The production DB is now a Postgres container on the same Docker/Swarm overlay network as the app, reachable as a bare service name. It does not speak TLS, and no managed-DB CA exists for it, so the previous fail-closed rules (CA required in production; DATABASE_SSL=disable refused in production) made it unusable. Narrow both rules to distinguish private from public hosts: a Docker service name, localhost, or an RFC1918 address may connect in plaintext, because that traffic never leaves the internal network. A routable hostname or public IP in production still fails closed exactly as before. Co-Authored-By: Claude Opus 5 (1M context) --- packages/db/src/index.ts | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index 40fa208..4264e64 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -20,6 +20,27 @@ function stripSslmode(url: string): string { } } +// A database reachable only over a private network (a Docker/Swarm service name, localhost, or an +// RFC1918 address) never crosses a link an attacker can sit on, so plaintext there is not the +// man-in-the-middle exposure that plaintext to a public host is. Anything else — a routable +// hostname or public IP — is treated as public and still fails closed in production. +function isPrivateHost(url: string): boolean { + let host: string; + try { + host = new URL(url).hostname; + } catch { + return false; + } + if (host === 'localhost' || host.endsWith('.internal') || host.endsWith('.local')) return true; + // Bare service name (no dots) — Docker/Swarm internal DNS, e.g. "elegalsoftware-db-puhy21". + if (!host.includes('.')) return true; + const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host); + if (!m) return false; + const a = Number(m[1]); + const b = Number(m[2]); + return a === 10 || a === 127 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168); +} + export function getPool(): pg.Pool { if (_pool) return _pool; @@ -36,17 +57,25 @@ export function getPool(): pg.Pool { // Without this, pg merges URL-derived settings which can conflict with the options below. let ssl: pg.PoolConfig['ssl']; if (process.env.DATABASE_SSL === 'disable') { - // Explicit opt-out for local dev/test databases that don't speak TLS at all (e.g. a - // disposable Docker Postgres). Refused in production — prod must always verify TLS. - if (isProd) { - throw new Error('DATABASE_SSL=disable is not allowed in production'); + // Explicit opt-out for databases that don't speak TLS at all (e.g. a disposable Docker + // Postgres). In production this is honored ONLY for a private-network host; pointing it at + // a public database still fails closed. + if (isProd && !isPrivateHost(connectionString)) { + throw new Error( + 'DATABASE_SSL=disable is only allowed in production when DATABASE_URL points at a private-network host (Docker service name, localhost, or an RFC1918 address).', + ); } ssl = false; } else if (ca) { // Verified TLS against the managed-DB CA — the correct posture everywhere. ssl = { ca, rejectUnauthorized: true }; + } else if (isProd && isPrivateHost(connectionString)) { + // Production database on a private network (app + Postgres on the same Docker/Swarm overlay). + // The connection never leaves that network, so plaintext is acceptable — and the managed-DB CA + // that verified TLS requires does not exist for a self-hosted container. + ssl = false; } else if (isProd) { - // Never run production against the database over unverified TLS: fail fast so a missing + // Public database host in production: never connect over unverified TLS. Fail fast so a missing // CA cert is a loud deploy error instead of a silent man-in-the-middle exposure. throw new Error( 'DATABASE_CA_CERT_PATH is required in production: point it at the managed-DB CA cert so TLS certificates are verified (rejectUnauthorized: true).',