Skip to content

Commit 15d3bdc

Browse files
authored
feat: Use standard TLS hostname validation for instances with DNS names (#428)
When the the Cloud SQL Instance reports that it has a DNS Name, the connector will use standard TLS hostname validation when checking the server certificate. Now, the server's TLS certificate must contain a SAN record with the instance's DNS name. The ConnectSettings API added a field dns_names which contains all of the valid DNS names for an instance. See also: GoogleCloudPlatform/cloud-sql-go-connector#954
1 parent cb1716e commit 15d3bdc

7 files changed

+64
-27
lines changed

package-lock.json

+7-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,9 @@
8383
"url": "git+https://github.com/GoogleCloudPlatform/cloud-sql-nodejs-connector"
8484
},
8585
"dependencies": {
86-
"@googleapis/sqladmin": "^24.0.0",
86+
"@googleapis/sqladmin": "^27.0.0",
8787
"gaxios": "^6.1.1",
8888
"google-auth-library": "^9.2.0",
8989
"p-throttle": "^7.0.0"
9090
}
91-
}
91+
}

src/connector.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,6 @@ export class Connector {
232232
port,
233233
privateKey,
234234
serverCaCert,
235-
serverCaMode,
236235
dnsName,
237236
} = cloudSqlInstance;
238237

@@ -251,8 +250,8 @@ export class Connector {
251250
port,
252251
privateKey,
253252
serverCaCert,
254-
serverCaMode,
255-
dnsName: instanceInfo.domainName || dnsName, // use the configured domain name, or the instance dnsName.
253+
instanceDnsName: dnsName,
254+
serverName: instanceInfo.domainName || dnsName, // use the configured domain name, or the instance dnsName.
256255
});
257256
tlsSocket.once('error', () => {
258257
cloudSqlInstance.forceRefresh();

src/socket.ts

+10-10
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,17 @@ interface SocketOptions {
2626
instanceInfo: InstanceConnectionInfo;
2727
privateKey: string;
2828
serverCaCert: SslCert;
29-
serverCaMode: string;
30-
dnsName: string;
29+
instanceDnsName: string;
30+
serverName: string;
3131
}
3232

3333
export function validateCertificate(
3434
instanceInfo: InstanceConnectionInfo,
35-
serverCaMode: string,
36-
dnsName: string
35+
instanceDnsName: string,
36+
serverName: string
3737
) {
3838
return (hostname: string, cert: tls.PeerCertificate): Error | undefined => {
39-
if (!serverCaMode || serverCaMode === 'GOOGLE_MANAGED_INTERNAL_CA') {
39+
if (!instanceDnsName) {
4040
// Legacy CA Mode
4141
if (!cert || !cert.subject) {
4242
return new CloudSQLConnectorError({
@@ -54,7 +54,7 @@ export function validateCertificate(
5454
return undefined;
5555
} else {
5656
// Standard TLS Verify Full hostname verification using SAN
57-
return tls.checkServerIdentity(dnsName, cert);
57+
return tls.checkServerIdentity(serverName, cert);
5858
}
5959
};
6060
}
@@ -66,8 +66,8 @@ export function getSocket({
6666
instanceInfo,
6767
privateKey,
6868
serverCaCert,
69-
serverCaMode,
70-
dnsName,
69+
instanceDnsName,
70+
serverName,
7171
}: SocketOptions): tls.TLSSocket {
7272
const socketOpts = {
7373
host,
@@ -80,8 +80,8 @@ export function getSocket({
8080
}),
8181
checkServerIdentity: validateCertificate(
8282
instanceInfo,
83-
serverCaMode,
84-
dnsName
83+
instanceDnsName,
84+
serverName
8585
),
8686
};
8787
const tlsSocket = tls.connect(socketOpts);

src/sqladmin-fetcher.ts

+30-2
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ export class SQLAdminFetcher {
132132
private parseIpAddresses(
133133
ipResponse: sqladmin_v1beta4.Schema$IpMapping[] | undefined,
134134
dnsName: string | null | undefined,
135+
dnsNames: sqladmin_v1beta4.Schema$DnsNameMapping[] | null | undefined,
135136
pscEnabled: boolean | null | undefined
136137
): IpAddresses {
137138
const ipAddresses: IpAddresses = {};
@@ -149,7 +150,23 @@ export class SQLAdminFetcher {
149150
// Resolve dnsName into IP address for PSC enabled instances.
150151
// Note that we have to check for PSC enablement because CAS instances
151152
// also set the dnsName field.
152-
if (dnsName && pscEnabled) {
153+
154+
// Search the dns_names field for the PSC DNS Name.
155+
if (dnsNames) {
156+
for (const dnm of dnsNames) {
157+
if (
158+
dnm.name &&
159+
dnm.connectionType === 'PRIVATE_SERVICE_CONNECT' &&
160+
dnm.dnsScope === 'INSTANCE'
161+
) {
162+
ipAddresses.psc = dnm.name;
163+
break;
164+
}
165+
}
166+
}
167+
168+
// If the psc dns name was not found, use the legacy dns_name field
169+
if (!ipAddresses.psc && dnsName && pscEnabled) {
153170
ipAddresses.psc = dnsName;
154171
}
155172

@@ -188,6 +205,7 @@ export class SQLAdminFetcher {
188205
const ipAddresses = this.parseIpAddresses(
189206
res.data.ipAddresses,
190207
res.data.dnsName,
208+
res.data.dnsNames,
191209
res.data.pscEnabled
192210
);
193211

@@ -214,6 +232,16 @@ export class SQLAdminFetcher {
214232
}
215233

216234
cleanGaxiosConfig();
235+
// Find a DNS name to use to validate the certificate from the dns_names field. Any
236+
// name in the list may be used to validate the server TLS certificate.
237+
// Fall back to legacy dns_name field if necessary.
238+
let serverName = null;
239+
if (res.data.dnsNames && res.data.dnsNames.length > 0) {
240+
serverName = res.data.dnsNames[0].name;
241+
}
242+
if (serverName === null) {
243+
serverName = res.data.dnsName;
244+
}
217245

218246
return {
219247
ipAddresses,
@@ -222,7 +250,7 @@ export class SQLAdminFetcher {
222250
expirationTime: serverCaCert.expirationTime,
223251
},
224252
serverCaMode: res.data.serverCaMode || '',
225-
dnsName: res.data.dnsName || '',
253+
dnsName: serverName || '',
226254
};
227255
}
228256

test/socket.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ t.test('validateCertificate no cert', async t => {
6969
regionId: 'region-id',
7070
instanceId: 'my-instance',
7171
},
72-
'GOOGLE_MANAGED_INTERNAL_CA',
72+
null,
7373
'abcde.12345.us-central1.sql.goog'
7474
)('hostname', {} as tls.PeerCertificate),
7575
{code: 'ENOSQLADMINVERIFYCERT'},
@@ -90,7 +90,7 @@ t.test('validateCertificate mismatch', async t => {
9090
regionId: 'region-id',
9191
instanceId: 'my-instance',
9292
},
93-
'GOOGLE_MANAGED_INTERNAL_CA',
93+
null,
9494
'abcde.12345.us-central1.sql.goog'
9595
)('hostname', cert),
9696
{

test/sqladmin-fetcher.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import t from 'tap';
1616
import {InstanceConnectionInfo} from '../src/instance-connection-info';
1717
import {CLIENT_CERT} from './fixtures/certs';
1818
import {AuthTypes} from '../src/auth-types';
19+
import {sqladmin_v1beta4} from '@googleapis/sqladmin';
20+
import Schema$DnsNameMapping = sqladmin_v1beta4.Schema$DnsNameMapping;
1921

2022
// Mocks the @googleapis/sqladmin interface
2123
interface SQLAdminOptions {
@@ -27,6 +29,7 @@ interface IpAddress {
2729
}
2830
interface SQLAdminClientGetResponse {
2931
dnsName?: string;
32+
dnsNames?: Schema$DnsNameMapping[];
3033
ipAddresses?: IpAddress[];
3134
pscEnabled?: boolean;
3235
region?: string;
@@ -98,7 +101,13 @@ const mockSQLAdminGetInstanceMetadata = (
98101

99102
sqlAdminClient.get = () => ({
100103
data: {
101-
dnsName: 'abcde.12345.us-central1.sql.goog',
104+
dnsNames: [
105+
{
106+
name: 'abcde.12345.us-central1.sql.goog',
107+
connectionType: 'PRIVATE_SERVICE_CONNECT',
108+
dnsScope: 'INSTANCE',
109+
},
110+
],
102111
ipAddresses: [
103112
{
104113
type: 'PRIMARY',
@@ -216,6 +225,7 @@ t.test('getInstanceMetadata no ip', async t => {
216225
};
217226
mockSQLAdminGetInstanceMetadata(instanceConnectionInfo, {
218227
dnsName: 'abcde.12345.us-central1.sql.goog',
228+
dnsNames: null,
219229
ipAddresses: [],
220230
pscEnabled: false,
221231
});

0 commit comments

Comments
 (0)