From ff5b6c540251236aa086573ada2c35d56915ab98 Mon Sep 17 00:00:00 2001 From: Bobbie Soedirgo Date: Fri, 25 Aug 2023 17:07:23 +0800 Subject: [PATCH] feat: allow setting root cert directly Set the root cert directly via env instead of setting the path of the cert. Also, sslrootcert will be applied on all pg connections, instead of the user having to set &sslrootcert= path in the connection string. --- src/lib/db.ts | 28 ++++++++++++++++++++++++++++ src/server/constants.ts | 18 +++++++++++++----- test/server/ssl.ts | 13 ++++++++++--- 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/src/lib/db.ts b/src/lib/db.ts index 44ebf0a8..52834b7a 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -23,6 +23,34 @@ export const init: (config: PoolConfig) => { query: (sql: string) => Promise> end: () => Promise } = (config) => { + // node-postgres ignores config.ssl if any of sslmode, sslca, sslkey, sslcert, + // sslrootcert are in the connection string. Here we allow setting sslmode in + // the connection string while setting the rest in config.ssl. + if (config.connectionString) { + const u = new URL(config.connectionString) + const sslmode = u.searchParams.get('sslmode') + u.searchParams.delete('sslmode') + // For now, we don't support setting these from the connection string. + u.searchParams.delete('sslca') + u.searchParams.delete('sslkey') + u.searchParams.delete('sslcert') + u.searchParams.delete('sslrootcert') + config.connectionString = u.toString() + + // sslmode: null, 'disable', 'prefer', 'require', 'verify-ca', 'verify-full', 'no-verify' + // config.ssl: true, false, {} + if (sslmode === null) { + // skip + } else if (sslmode === 'disable') { + config.ssl = false + } else { + if (typeof config.ssl !== 'object') { + config.ssl = {} + } + config.ssl.rejectUnauthorized = sslmode !== 'no-verify' + } + } + // NOTE: Race condition could happen here: one async task may be doing // `pool.end()` which invalidates the pool and subsequently all existing // handles to `query`. Normally you might only deal with one DB so you don't diff --git a/src/server/constants.ts b/src/server/constants.ts index b33f2abf..e6abcde0 100644 --- a/src/server/constants.ts +++ b/src/server/constants.ts @@ -1,3 +1,5 @@ +import crypto from 'crypto' +import { PoolConfig } from 'pg' import { getSecret } from '../lib/secrets.js' export const PG_META_HOST = process.env.PG_META_HOST || '0.0.0.0' @@ -10,7 +12,6 @@ const PG_META_DB_USER = process.env.PG_META_DB_USER || 'postgres' const PG_META_DB_PORT = process.env.PG_META_DB_PORT || '5432' const PG_META_DB_PASSWORD = (await getSecret('PG_META_DB_PASSWORD')) || 'postgres' const PG_META_DB_SSL_MODE = process.env.PG_META_DB_SSL_MODE || 'disable' -const PG_META_DB_SSL_ROOT_CERT_PATH = process.env.PG_META_DB_SSL_ROOT_CERT_PATH const PG_CONN_TIMEOUT_SECS = Number(process.env.PG_CONN_TIMEOUT_SECS || 15) @@ -23,17 +24,24 @@ if (!PG_CONNECTION) { pgConn.password = PG_META_DB_PASSWORD pgConn.pathname = encodeURIComponent(PG_META_DB_NAME) pgConn.searchParams.set('sslmode', PG_META_DB_SSL_MODE) - if (PG_META_DB_SSL_ROOT_CERT_PATH) { - pgConn.searchParams.set('sslrootcert', PG_META_DB_SSL_ROOT_CERT_PATH) - } PG_CONNECTION = `${pgConn}` } +export const PG_META_DB_SSL_ROOT_CERT = process.env.PG_META_DB_SSL_ROOT_CERT +if (PG_META_DB_SSL_ROOT_CERT) { + // validate cert + new crypto.X509Certificate(PG_META_DB_SSL_ROOT_CERT) +} + export const EXPORT_DOCS = process.argv[2] === 'docs' && process.argv[3] === 'export' export const GENERATE_TYPES = process.argv[2] === 'gen' && process.argv[3] === 'types' ? process.argv[4] : undefined export const GENERATE_TYPES_INCLUDED_SCHEMAS = GENERATE_TYPES && process.argv[5] === '--include-schemas' ? process.argv[6]?.split(',') ?? [] : [] -export const DEFAULT_POOL_CONFIG = { max: 1, connectionTimeoutMillis: PG_CONN_TIMEOUT_SECS * 1000 } +export const DEFAULT_POOL_CONFIG: PoolConfig = { + max: 1, + connectionTimeoutMillis: PG_CONN_TIMEOUT_SECS * 1000, +} + export const PG_META_REQ_HEADER = process.env.PG_META_REQ_HEADER || 'request-id' diff --git a/test/server/ssl.ts b/test/server/ssl.ts index 640ae103..063b8a86 100644 --- a/test/server/ssl.ts +++ b/test/server/ssl.ts @@ -1,12 +1,14 @@ import CryptoJS from 'crypto-js' +import fs from 'fs' import path from 'path' import { fileURLToPath } from 'url' import { app } from './utils' -import { CRYPTO_KEY } from '../../src/server/constants' +import { CRYPTO_KEY, DEFAULT_POOL_CONFIG } from '../../src/server/constants' // @ts-ignore: Harmless type error on import.meta. const cwd = path.dirname(fileURLToPath(import.meta.url)) -const SSL_ROOT_CERT_PATH = path.join(cwd, '../db/server.crt') +const sslRootCertPath = path.join(cwd, '../db/server.crt') +const sslRootCert = fs.readFileSync(sslRootCertPath, { encoding: 'utf8' }) test('query with no ssl', async () => { const res = await app.inject({ @@ -45,12 +47,15 @@ test('query with ssl w/o root cert', async () => { }) test('query with ssl with root cert', async () => { + const defaultSsl = DEFAULT_POOL_CONFIG.ssl + DEFAULT_POOL_CONFIG.ssl = { ca: sslRootCert } + const res = await app.inject({ method: 'POST', path: '/query', headers: { 'x-connection-encrypted': CryptoJS.AES.encrypt( - `postgresql://postgres:postgres@localhost:5432/postgres?sslmode=verify-full&sslrootcert=${SSL_ROOT_CERT_PATH}`, + `postgresql://postgres:postgres@localhost:5432/postgres?sslmode=verify-full`, CRYPTO_KEY ).toString(), }, @@ -63,4 +68,6 @@ test('query with ssl with root cert', async () => { }, ] `) + + DEFAULT_POOL_CONFIG.ssl = defaultSsl })