11import { type DrizzleConfig } from 'drizzle-orm' ;
22import { drizzle } from 'drizzle-orm/node-postgres' ;
3+ import { type PeerCertificate , checkServerIdentity as tlsCheckServerIdentity } from 'node:tls' ;
34import { env } from '../env' ;
45
56/**
@@ -15,7 +16,7 @@ const dbConfig: DrizzleConfig = {
1516// misconfiguration we fail fast on rather than silently downgrading security.
1617// The secret is base64-encoded (the PEM is multi-line and would break the
1718// line-based `.env.runtime` delivery), so decode it back to PEM here.
18- const sslConfig =
19+ const sslCa =
1920 env . NODE_ENV === 'production'
2021 ? ( ( ) => {
2122 if ( ! env . DATABASE_SSL_CA ) {
@@ -25,23 +26,55 @@ const sslConfig =
2526 "CLI → 'Apply infra change', or check the database-ssl-ca runtime secret." ,
2627 ) ;
2728 }
28- return { ca : Buffer . from ( env . DATABASE_SSL_CA , 'base64' ) . toString ( 'utf-8' ) , rejectUnauthorized : true } ;
29+ return Buffer . from ( env . DATABASE_SSL_CA , 'base64' ) . toString ( 'utf-8' ) ;
2930 } ) ( )
3031 : undefined ;
3132
3233// Scaleway-built connection strings pin `sslmode=require&uselibpqcompat=true`,
33- // which pg would let override the verified `ssl` config above. Strip those
34- // params so our CA-pinned config wins.
35- const connectionString = ( ( ) => {
34+ // which pg would let override the verified `ssl` config below (downgrading to
35+ // no certificate verification). Strip those params so our CA-pinned config wins.
36+ export const stripSslParams = ( url : string ) : string => {
3637 try {
37- const parsed = new URL ( env . DATABASE_CDC_URL ) ;
38+ const parsed = new URL ( url ) ;
3839 parsed . searchParams . delete ( 'sslmode' ) ;
3940 parsed . searchParams . delete ( 'uselibpqcompat' ) ;
4041 return parsed . toString ( ) ;
4142 } catch {
42- return env . DATABASE_CDC_URL ;
43+ return url ;
4344 }
44- } ) ( ) ;
45+ } ;
46+
47+ /** Parse the host (private IP or DNS name) from a postgres connection string. */
48+ const hostOf = ( connectionString : string ) : string | undefined => {
49+ try {
50+ return new URL ( stripSslParams ( connectionString ) ) . hostname || undefined ;
51+ } catch {
52+ return undefined ;
53+ }
54+ } ;
55+
56+ /**
57+ * Build the verified-TLS `ssl` option for one connection. We pin the CA and keep
58+ * `rejectUnauthorized` so the chain is fully verified. The Scaleway RDB cert
59+ * carries proper SANs (e.g. `DNS:10.0.0.2, IP Address:10.0.0.2`), so the only
60+ * problem is WHICH host the identity check runs against: node-postgres does not
61+ * thread the connection host into the TLS layer, so the check defaults to
62+ * `localhost` and fails (ERR_TLS_CERT_ALTNAME_INVALID) even though the cert
63+ * legitimately covers the host we dialed. Pin the identity check to that actual
64+ * host so the cert's real SANs are honored (chain still verified against the CA).
65+ * Returns `undefined` outside production, where TLS is not required.
66+ */
67+ export const buildVerifiedSsl = ( connectionString : string ) => {
68+ if ( ! sslCa ) return undefined ;
69+ const host = hostOf ( connectionString ) ;
70+ return {
71+ ca : sslCa ,
72+ rejectUnauthorized : true ,
73+ checkServerIdentity : host ? ( _passedHost : string , cert : PeerCertificate ) => tlsCheckServerIdentity ( host , cert ) : undefined ,
74+ } ;
75+ } ;
76+
77+ const connectionString = stripSslParams ( env . DATABASE_CDC_URL ) ;
4578
4679/**
4780 * CDC database client.
@@ -53,6 +86,6 @@ const connectionString = (() => {
5386 * privileges.
5487 */
5588export const cdcDb = drizzle ( {
56- connection : { connectionString, connectionTimeoutMillis : 10_000 , max : 20 , ssl : sslConfig } ,
89+ connection : { connectionString, connectionTimeoutMillis : 10_000 , max : 20 , ssl : buildVerifiedSsl ( env . DATABASE_CDC_URL ) } ,
5790 ...dbConfig ,
5891} ) ;
0 commit comments