Skip to content

Commit 5d2c02f

Browse files
feat: support Cloud SQL CAS-based instances (#390)
The CAS instances will have a different way to verify the server identity. They will use the dnsName of the Cloud SQL instance as a SAN.
1 parent 4e3f31b commit 5d2c02f

File tree

10 files changed

+182
-12
lines changed

10 files changed

+182
-12
lines changed

.github/workflows/tests.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ jobs:
181181
POSTGRES_IAM_USER:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_USER_IAM_NODE
182182
POSTGRES_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_PASS
183183
POSTGRES_DB:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_DB
184+
POSTGRES_CAS_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CAS_CONNECTION_NAME
185+
POSTGRES_CAS_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CAS_PASS
184186
SQLSERVER_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_CONNECTION_NAME
185187
SQLSERVER_USER:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_USER
186188
SQLSERVER_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_PASS
@@ -212,6 +214,8 @@ jobs:
212214
POSTGRES_IAM_USER: "${{ steps.secrets.outputs.POSTGRES_IAM_USER }}"
213215
POSTGRES_PASS: "${{ steps.secrets.outputs.POSTGRES_PASS }}"
214216
POSTGRES_DB: "${{ steps.secrets.outputs.POSTGRES_DB }}"
217+
POSTGRES_CAS_CONNECTION_NAME: "${{ steps.secrets.outputs.POSTGRES_CAS_CONNECTION_NAME }}"
218+
POSTGRES_CAS_PASS: "${{ steps.secrets.outputs.POSTGRES_CAS_PASS }}"
215219
SQLSERVER_CONNECTION_NAME: "${{ steps.secrets.outputs.SQLSERVER_CONNECTION_NAME }}"
216220
SQLSERVER_USER: "${{ steps.secrets.outputs.SQLSERVER_USER }}"
217221
SQLSERVER_PASS: "${{ steps.secrets.outputs.SQLSERVER_PASS }}"
@@ -234,6 +238,8 @@ jobs:
234238
POSTGRES_IAM_USER: "${{ steps.secrets.outputs.POSTGRES_IAM_USER }}"
235239
POSTGRES_PASS: "${{ steps.secrets.outputs.POSTGRES_PASS }}"
236240
POSTGRES_DB: "${{ steps.secrets.outputs.POSTGRES_DB }}"
241+
POSTGRES_CAS_CONNECTION_NAME: "${{ steps.secrets.outputs.POSTGRES_CAS_CONNECTION_NAME }}"
242+
POSTGRES_CAS_PASS: "${{ steps.secrets.outputs.POSTGRES_CAS_PASS }}"
237243
SQLSERVER_CONNECTION_NAME: "${{ steps.secrets.outputs.SQLSERVER_CONNECTION_NAME }}"
238244
SQLSERVER_USER: "${{ steps.secrets.outputs.SQLSERVER_USER }}"
239245
SQLSERVER_PASS: "${{ steps.secrets.outputs.SQLSERVER_PASS }}"

src/cloud-sql-instance.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ export class CloudSQLInstance {
7575
public port = 3307;
7676
public privateKey?: string;
7777
public serverCaCert?: SslCert;
78+
public serverCaMode = '';
79+
public dnsName = '';
7880

7981
constructor({
8082
ipType,
@@ -193,6 +195,8 @@ export class CloudSQLInstance {
193195
const host = selectIpAddress(metadata.ipAddresses, this.ipType);
194196
const privateKey = rsaKeys.privateKey;
195197
const serverCaCert = metadata.serverCaCert;
198+
this.serverCaMode = metadata.serverCaMode;
199+
this.dnsName = metadata.dnsName;
196200

197201
const currentValues = {
198202
ephemeralCert: this.ephemeralCert,

src/connector.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ export class Connector {
206206
port,
207207
privateKey,
208208
serverCaCert,
209+
serverCaMode,
210+
dnsName,
209211
} = cloudSqlInstance;
210212

211213
if (
@@ -223,6 +225,8 @@ export class Connector {
223225
port,
224226
privateKey,
225227
serverCaCert,
228+
serverCaMode,
229+
dnsName,
226230
});
227231
tlsSocket.once('error', async () => {
228232
await cloudSqlInstance.forceRefresh();

src/socket.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,19 @@ interface SocketOptions {
2626
instanceInfo: InstanceConnectionInfo;
2727
privateKey: string;
2828
serverCaCert: SslCert;
29+
serverCaMode: string;
30+
dnsName: string;
2931
}
3032

31-
export function validateCertificate(instanceInfo: InstanceConnectionInfo) {
33+
export function validateCertificate(
34+
instanceInfo: InstanceConnectionInfo,
35+
serverCaMode: string,
36+
dnsName: string
37+
) {
3238
return (hostname: string, cert: tls.PeerCertificate): Error | undefined => {
39+
if (serverCaMode === 'GOOGLE_MANAGED_CAS_CA') {
40+
return tls.checkServerIdentity(dnsName, cert);
41+
}
3342
if (!cert || !cert.subject) {
3443
return new CloudSQLConnectorError({
3544
message: 'No certificate to verify',
@@ -54,6 +63,8 @@ export function getSocket({
5463
instanceInfo,
5564
privateKey,
5665
serverCaCert,
66+
serverCaMode,
67+
dnsName,
5768
}: SocketOptions): tls.TLSSocket {
5869
const socketOpts = {
5970
host,
@@ -64,7 +75,11 @@ export function getSocket({
6475
key: privateKey,
6576
minVersion: 'TLSv1.3',
6677
}),
67-
checkServerIdentity: validateCertificate(instanceInfo),
78+
checkServerIdentity: validateCertificate(
79+
instanceInfo,
80+
serverCaMode,
81+
dnsName
82+
),
6883
};
6984
const tlsSocket = tls.connect(socketOpts);
7085
tlsSocket.setKeepAlive(true, DEFAULT_KEEP_ALIVE_DELAY_MS);

src/sqladmin-fetcher.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import {AuthTypes} from './auth-types';
2727
export interface InstanceMetadata {
2828
ipAddresses: IpAddresses;
2929
serverCaCert: SslCert;
30+
serverCaMode: string;
31+
dnsName: string;
3032
}
3133

3234
interface RequestBody {
@@ -216,6 +218,8 @@ export class SQLAdminFetcher {
216218
cert: serverCaCert.cert,
217219
expirationTime: serverCaCert.expirationTime,
218220
},
221+
serverCaMode: res.data.serverCaMode || '',
222+
dnsName: res.data.dnsName || '',
219223
};
220224
}
221225

system-test/pg-connect.cjs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,31 @@ t.test('open IAM connection and retrieves standard pg tables', async t => {
6565
await client.end();
6666
connector.close();
6767
});
68+
69+
t.test(
70+
'open connection to CAS-based CA instance and retrieves standard pg tables',
71+
async t => {
72+
const connector = new Connector();
73+
const clientOpts = await connector.getOptions({
74+
instanceConnectionName: process.env.POSTGRES_CAS_CONNECTION_NAME,
75+
ipType: 'PUBLIC',
76+
authType: 'PASSWORD',
77+
});
78+
const client = new Client({
79+
...clientOpts,
80+
user: process.env.POSTGRES_USER,
81+
password: process.env.POSTGRES_CAS_PASS,
82+
database: process.env.POSTGRES_DB,
83+
});
84+
client.connect();
85+
86+
const {
87+
rows: [result],
88+
} = await client.query('SELECT NOW();');
89+
const returnedDate = result['now'];
90+
t.ok(returnedDate.getTime(), 'should have valid returned date object');
91+
92+
await client.end();
93+
connector.close();
94+
}
95+
);

system-test/pg-connect.mjs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,31 @@ t.test('open IAM connection and retrieves standard pg tables', async t => {
6565
await client.end();
6666
connector.close();
6767
});
68+
69+
t.test(
70+
'open connection to CAS-based CA instance and retrieves standard pg tables',
71+
async t => {
72+
const connector = new Connector();
73+
const clientOpts = await connector.getOptions({
74+
instanceConnectionName: process.env.POSTGRES_CAS_CONNECTION_NAME,
75+
ipType: 'PUBLIC',
76+
authType: 'PASSWORD',
77+
});
78+
const client = new Client({
79+
...clientOpts,
80+
user: process.env.POSTGRES_USER,
81+
password: process.env.POSTGRES_CAS_PASS,
82+
database: process.env.POSTGRES_DB,
83+
});
84+
client.connect();
85+
86+
const {
87+
rows: [result],
88+
} = await client.query('SELECT NOW();');
89+
const returnedDate = result['now'];
90+
t.ok(returnedDate.getTime(), 'should have valid returned date object');
91+
92+
await client.end();
93+
connector.close();
94+
}
95+
);

system-test/pg-connect.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,29 @@ t.test('open IAM connection and retrieves standard pg tables', async t => {
6767
await client.end();
6868
connector.close();
6969
});
70+
71+
t.test(
72+
'open connection to CAS-based CA instance and retrieves standard pg tables',
73+
async t => {
74+
const connector = new Connector();
75+
const clientOpts = await connector.getOptions({
76+
instanceConnectionName: String(process.env.POSTGRES_CAS_CONNECTION_NAME),
77+
});
78+
const client = new Client({
79+
...clientOpts,
80+
user: String(process.env.POSTGRES_USER),
81+
password: String(process.env.POSTGRES_CAS_PASS),
82+
database: String(process.env.POSTGRES_DB),
83+
});
84+
client.connect();
85+
86+
const {
87+
rows: [result],
88+
} = await client.query('SELECT NOW();');
89+
const returnedDate = result['now'];
90+
t.ok(returnedDate.getTime(), 'should have valid returned date object');
91+
92+
await client.end();
93+
connector.close();
94+
}
95+
);

test/socket.ts

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ t.test('getSocket', async t => {
4141
cert: CA_CERT,
4242
expirationTime: '2033-01-06T10:00:00.232Z',
4343
},
44+
serverCaMode: 'GOOGLE_MANAGED_INTERNAL_CA',
45+
dnsName: 'abcde.12345.us-central1.sql.goog',
4446
});
4547

4648
socket.on('secureConnect', () => {
@@ -61,11 +63,15 @@ t.test('getSocket', async t => {
6163

6264
t.test('validateCertificate no cert', async t => {
6365
t.match(
64-
validateCertificate({
65-
projectId: 'my-project',
66-
regionId: 'region-id',
67-
instanceId: 'my-instance',
68-
})('hostname', {} as tls.PeerCertificate),
66+
validateCertificate(
67+
{
68+
projectId: 'my-project',
69+
regionId: 'region-id',
70+
instanceId: 'my-instance',
71+
},
72+
'GOOGLE_MANAGED_INTERNAL_CA',
73+
'abcde.12345.us-central1.sql.goog'
74+
)('hostname', {} as tls.PeerCertificate),
6975
{code: 'ENOSQLADMINVERIFYCERT'},
7076
'should return a missing cert to verify error'
7177
);
@@ -78,11 +84,15 @@ t.test('validateCertificate mismatch', async t => {
7884
},
7985
} as tls.PeerCertificate;
8086
t.match(
81-
validateCertificate({
82-
projectId: 'my-project',
83-
regionId: 'region-id',
84-
instanceId: 'my-instance',
85-
})('hostname', cert),
87+
validateCertificate(
88+
{
89+
projectId: 'my-project',
90+
regionId: 'region-id',
91+
instanceId: 'my-instance',
92+
},
93+
'GOOGLE_MANAGED_INTERNAL_CA',
94+
'abcde.12345.us-central1.sql.goog'
95+
)('hostname', cert),
8696
{
8797
message:
8898
'Certificate had CN other-project:other-instance, expected my-project:my-instance',
@@ -91,3 +101,45 @@ t.test('validateCertificate mismatch', async t => {
91101
'should return a missing cert to verify error'
92102
);
93103
});
104+
105+
t.test('validateCertificate mismatch CAS CA', async t => {
106+
const cert = {
107+
subjectaltname: 'DNS:abcde.12345.us-central1.sql.goog',
108+
} as tls.PeerCertificate;
109+
t.match(
110+
validateCertificate(
111+
{
112+
projectId: 'my-project',
113+
regionId: 'region-id',
114+
instanceId: 'my-instance',
115+
},
116+
'GOOGLE_MANAGED_CAS_CA',
117+
'bad.dns.us-central1.sql.goog'
118+
)('hostname', cert),
119+
{
120+
message:
121+
"Hostname/IP does not match certificate's altnames: Host: bad.dns.us-central1.sql.goog. is not in the cert's altnames: DNS:abcde.12345.us-central1.sql.goog",
122+
code: 'ERR_TLS_CERT_ALTNAME_INVALID',
123+
},
124+
'should return an invalid altname error'
125+
);
126+
});
127+
128+
t.test('validateCertificate valid CAS CA', async t => {
129+
const cert = {
130+
subjectaltname: 'DNS:abcde.12345.us-central1.sql.goog',
131+
} as tls.PeerCertificate;
132+
t.match(
133+
validateCertificate(
134+
{
135+
projectId: 'my-project',
136+
regionId: 'region-id',
137+
instanceId: 'my-instance',
138+
},
139+
'GOOGLE_MANAGED_CAS_CA',
140+
'abcde.12345.us-central1.sql.goog'
141+
)('hostname', cert),
142+
undefined,
143+
'DNS name matches SAN in cert'
144+
);
145+
});

test/sqladmin-fetcher.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ const mockSQLAdminGetInstanceMetadata = (
116116
pscEnabled: true,
117117
region: regionId,
118118
serverCaCert: serverCaCertResponse(instanceId),
119+
serverCaMode: 'GOOGLE_MANAGED_INTERNAL_CA',
119120
...overrides,
120121
},
121122
});
@@ -189,6 +190,8 @@ t.test('getInstanceMetadata', async t => {
189190
cert: '-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----',
190191
expirationTime: '2033-01-06T10:00:00.232Z',
191192
},
193+
serverCaMode: 'GOOGLE_MANAGED_INTERNAL_CA',
194+
dnsName: 'abcde.12345.us-central1.sql.goog',
192195
},
193196
'should return expected instance metadata object'
194197
);

0 commit comments

Comments
 (0)