Allow plaintext Postgres in production for private-network hosts only
CI / build-and-test (push) Canceled after 0s
CI / build-and-test (push) Canceled after 0s
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) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
434de547ce
commit
605c0e0052
@@ -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 {
|
export function getPool(): pg.Pool {
|
||||||
if (_pool) return _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.
|
// Without this, pg merges URL-derived settings which can conflict with the options below.
|
||||||
let ssl: pg.PoolConfig['ssl'];
|
let ssl: pg.PoolConfig['ssl'];
|
||||||
if (process.env.DATABASE_SSL === 'disable') {
|
if (process.env.DATABASE_SSL === 'disable') {
|
||||||
// Explicit opt-out for local dev/test databases that don't speak TLS at all (e.g. a
|
// Explicit opt-out for databases that don't speak TLS at all (e.g. a disposable Docker
|
||||||
// disposable Docker Postgres). Refused in production — prod must always verify TLS.
|
// Postgres). In production this is honored ONLY for a private-network host; pointing it at
|
||||||
if (isProd) {
|
// a public database still fails closed.
|
||||||
throw new Error('DATABASE_SSL=disable is not allowed in production');
|
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;
|
ssl = false;
|
||||||
} else if (ca) {
|
} else if (ca) {
|
||||||
// Verified TLS against the managed-DB CA — the correct posture everywhere.
|
// Verified TLS against the managed-DB CA — the correct posture everywhere.
|
||||||
ssl = { ca, rejectUnauthorized: true };
|
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) {
|
} 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.
|
// CA cert is a loud deploy error instead of a silent man-in-the-middle exposure.
|
||||||
throw new Error(
|
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).',
|
'DATABASE_CA_CERT_PATH is required in production: point it at the managed-DB CA cert so TLS certificates are verified (rejectUnauthorized: true).',
|
||||||
|
|||||||
Reference in New Issue
Block a user