1+ import fs from "node:fs" ;
12import net from "node:net" ;
23import tls from "node:tls" ;
34
@@ -12,6 +13,8 @@ export interface HyperdriveProxyConfig {
1213 scheme : string ;
1314 // The sslmode of the target database
1415 sslmode : string ;
16+ // The path to the SSL root certificate, or "system" to use the system CA store
17+ sslrootcert ?: string ;
1518}
1619
1720const schemes = {
@@ -25,11 +28,85 @@ export const POSTGRES_SSL_REQUEST_PACKET = Buffer.from([
2528 0x00 , 0x00 , 0x00 , 0x08 , 0x04 , 0xd2 , 0x16 , 0x2f ,
2629] ) ;
2730
31+ interface TlsConfig {
32+ rejectUnauthorized : boolean ;
33+ ca ?: Buffer < ArrayBuffer > ;
34+ checkServerIdentity ?: ( ) => undefined ;
35+ }
36+
37+ /**
38+ * Builds TLS configuration based on sslmode and sslrootcert settings.
39+ *
40+ * - verify-full: validates the server certificate against CA and checks hostname
41+ * - verify-ca: validates the server certificate against CA but skips hostname check
42+ * - require/prefer: connects with TLS but does not validate the server certificate
43+ * - sslrootcert=system: uses Node.js default system CA store
44+ * - sslrootcert=<path>: reads the CA certificate from the specified file path
45+ */
46+ function buildTlsConfig (
47+ sslmodeVerifyFull : boolean ,
48+ sslmodeVerifyCa : boolean ,
49+ sslrootcert ?: string
50+ ) : TlsConfig {
51+ if ( ! sslmodeVerifyFull && ! sslmodeVerifyCa ) {
52+ return { rejectUnauthorized : false } ;
53+ }
54+
55+ const config : TlsConfig = { rejectUnauthorized : true } ;
56+
57+ // Load custom CA certificate if sslrootcert is a file path (not "system")
58+ if ( sslrootcert && sslrootcert !== "system" ) {
59+ config . ca = fs . readFileSync ( sslrootcert ) ;
60+ }
61+ // When sslrootcert=system (or omitted for verify modes), Node.js uses its
62+ // default system CA store automatically, so no explicit ca is needed.
63+
64+ if ( sslmodeVerifyCa ) {
65+ // verify-ca validates the certificate chain but does not check the hostname
66+ config . checkServerIdentity = ( ) => undefined ;
67+ }
68+
69+ return config ;
70+ }
71+
72+ /**
73+ * Assembles a complete `tls.ConnectionOptions` from the base socket/host info
74+ * and the verification policy captured in `TlsConfig`.
75+ *
76+ * Keeping this separate from `buildTlsConfig` lets each protocol handler
77+ * (Postgres, MySQL) call it with their own socket without duplicating the
78+ * CA / hostname-verification logic inline.
79+ */
80+ function buildTlsConnectionOptions (
81+ dbSocket : net . Socket ,
82+ targetHost : string ,
83+ tlsConfig : TlsConfig ,
84+ extra ?: Partial < tls . ConnectionOptions >
85+ ) : tls . ConnectionOptions {
86+ const options : tls . ConnectionOptions = {
87+ socket : dbSocket ,
88+ host : targetHost ,
89+ servername : targetHost ,
90+ rejectUnauthorized : tlsConfig . rejectUnauthorized ,
91+ ...extra ,
92+ } ;
93+
94+ if ( tlsConfig . ca ) {
95+ options . ca = tlsConfig . ca ;
96+ }
97+
98+ if ( tlsConfig . checkServerIdentity ) {
99+ options . checkServerIdentity = tlsConfig . checkServerIdentity ;
100+ }
101+
102+ return options ;
103+ }
104+
28105/**
29106 * HyperdriveProxyController establishes TLS-enabled connections between workerd
30107 * and external Postgres/MySQL databases. Supports PostgreSQL sslmode options
31- * ('require', 'prefer', 'disable') by proxying each Hyperdrive binding through
32- * a randomly assigned local port.
108+ * ('require', 'prefer', 'disable', 'verify-full', 'verify-ca' ) by proxying
109+ * each Hyperdrive binding through a randomly assigned local port.
33110 */
34111export class HyperdriveProxyController {
35112 // Map hyperdrive binding name to proxy server
@@ -42,14 +119,16 @@ export class HyperdriveProxyController {
42119 * @returns A promise that resolves to the port number of the proxy server.
43120 */
44121 async createProxyServer ( config : HyperdriveProxyConfig ) : Promise < number > {
45- const { name, targetHost, targetPort, scheme, sslmode } = config ;
122+ const { name, targetHost, targetPort, scheme, sslmode, sslrootcert } =
123+ config ;
46124 const server = net . createServer ( ( clientSocket ) => {
47125 this . #handleConnection(
48126 clientSocket ,
49127 targetHost ,
50128 Number . parseInt ( targetPort ) ,
51129 scheme ,
52- sslmode
130+ sslmode ,
131+ sslrootcert
53132 ) ;
54133 } ) ;
55134 const port = await new Promise < number > ( ( resolve , reject ) => {
@@ -74,39 +153,53 @@ export class HyperdriveProxyController {
74153 * @param targetPort - The port of the target database.
75154 * @param scheme - The scheme of the target database.
76155 * @param sslmode - The sslmode of the target database.
156+ * @param sslrootcert - Path to the SSL root certificate, or "system" to use the system CA store.
77157 */
78158 async #handleConnection(
79159 clientSocket : net . Socket ,
80160 targetHost : string ,
81161 targetPort : number ,
82162 scheme : string ,
83- sslmode : string
163+ sslmode : string ,
164+ sslrootcert ?: string
84165 ) {
85166 // Connect to real database
86167 const dbSocket = net . connect ( { host : targetHost , port : targetPort } ) ;
87168 const sslmodeRequire = sslmode === "require" ;
88169 const sslmodePrefer = sslmode === "prefer" ;
89- if ( sslmodePrefer || sslmodeRequire ) {
170+ const sslmodeVerifyFull = sslmode === "verify-full" ;
171+ const sslmodeVerifyCa = sslmode === "verify-ca" ;
172+ // verify-full and verify-ca are strict modes that must always attempt TLS
173+ const sslmodeStrict = sslmodeVerifyFull || sslmodeVerifyCa ;
174+
175+ if ( sslmodePrefer || sslmodeRequire || sslmodeStrict ) {
176+ const tlsConfig = buildTlsConfig (
177+ sslmodeVerifyFull ,
178+ sslmodeVerifyCa ,
179+ sslrootcert
180+ ) ;
90181 try {
91182 if ( scheme === schemes . postgres || scheme === schemes . postgresql ) {
92183 return await handlePostgresTlsConnection (
93184 dbSocket ,
94185 clientSocket ,
95186 targetHost ,
96187 targetPort ,
97- sslmodeRequire
188+ sslmodeRequire || sslmodeStrict ,
189+ tlsConfig
98190 ) ;
99191 } else if ( scheme === schemes . mysql ) {
100192 return await handleMySQLTlsConnection (
101193 dbSocket ,
102194 clientSocket ,
103195 targetHost ,
104196 targetPort ,
105- sslmodeRequire
197+ sslmodeRequire || sslmodeStrict ,
198+ tlsConfig
106199 ) ;
107200 }
108201 } catch ( e ) {
109- if ( sslmodeRequire ) {
202+ if ( sslmodeRequire || sslmodeStrict ) {
110203 // Write error to client so worker can read it
111204 clientSocket . write ( `${ e } \n` ) ;
112205 clientSocket . end ( ) ;
@@ -139,7 +232,8 @@ async function handlePostgresTlsConnection(
139232 clientSocket : net . Socket ,
140233 targetHost : string ,
141234 targetPort : number ,
142- sslmodeRequire : boolean
235+ sslmodeRequire : boolean ,
236+ tlsConfig : TlsConfig
143237) {
144238 // Send Postgres sslrequest bytes
145239 await writeAsync ( dbSocket , POSTGRES_SSL_REQUEST_PACKET ) ;
@@ -148,12 +242,11 @@ async function handlePostgresTlsConnection(
148242 // Read first byte ssl flag
149243 const sslResponseFlag = response . toString ( "utf8" , 0 , 1 ) ;
150244 if ( sslResponseFlag === "S" ) {
151- const tlsOptions : tls . ConnectionOptions = {
152- socket : dbSocket ,
153- host : targetHost ,
154- servername : targetHost ,
155- rejectUnauthorized : false ,
156- } ;
245+ const tlsOptions = buildTlsConnectionOptions (
246+ dbSocket ,
247+ targetHost ,
248+ tlsConfig
249+ ) ;
157250 try {
158251 const tlsSocket = await tlsConnect ( tlsOptions ) ;
159252 setupTLSConnection ( clientSocket , tlsSocket ) ;
@@ -187,7 +280,8 @@ async function handleMySQLTlsConnection(
187280 clientSocket : net . Socket ,
188281 targetHost : string ,
189282 targetPort : number ,
190- sslmodeRequire : boolean
283+ sslmodeRequire : boolean ,
284+ tlsConfig : TlsConfig
191285) {
192286 const initPacketChunk = await readAsync ( dbSocket ) ;
193287 // Little-endian parse payload header length
@@ -238,13 +332,12 @@ async function handleMySQLTlsConnection(
238332 await writeAsync ( dbSocket , sslRequestPacket ) ;
239333
240334 // Upgrade server connection to TLS
241- const tlsOptions : tls . ConnectionOptions = {
242- socket : dbSocket ,
243- host : targetHost ,
244- servername : targetHost ,
245- minVersion : "TLSv1.2" ,
246- rejectUnauthorized : false ,
247- } ;
335+ const tlsOptions = buildTlsConnectionOptions (
336+ dbSocket ,
337+ targetHost ,
338+ tlsConfig ,
339+ { minVersion : "TLSv1.2" }
340+ ) ;
248341
249342 try {
250343 const tlsSocket = await tlsConnect ( tlsOptions ) ;
0 commit comments