Skip to content

Commit d733a16

Browse files
authored
feat: Use standard TLS hostname validation for instances with DNS names (#954)
When 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.
1 parent 26dcf57 commit d733a16

File tree

9 files changed

+186
-109
lines changed

9 files changed

+186
-109
lines changed

dialer_test.go

Lines changed: 77 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -970,6 +970,8 @@ func (r *fakeResolver) Resolve(_ context.Context, name string) (instance.ConnNam
970970
func TestDialerSuccessfullyDialsDnsTxtRecord(t *testing.T) {
971971
inst := mock.NewFakeCSQLInstance(
972972
"my-project", "my-region", "my-instance",
973+
mock.WithDNSMapping("db.example.com", "INSTANCE", "CUSTOM_SAN"),
974+
mock.WithDNSMapping("db2.example.com", "INSTANCE", "CUSTOM_SAN"),
973975
)
974976
wantName, _ := instance.ParseConnNameWithDomainName("my-project:my-region:my-instance", "db.example.com")
975977
wantName2, _ := instance.ParseConnNameWithDomainName("my-project:my-region:my-instance", "db2.example.com")
@@ -1046,9 +1048,11 @@ func TestDialerUpdatesAutomaticallyAfterDnsChange(t *testing.T) {
10461048
// SRV record and connect to the correct instance.
10471049
inst := mock.NewFakeCSQLInstance(
10481050
"my-project", "my-region", "my-instance",
1051+
mock.WithDNS("update.example.com"),
10491052
)
10501053
inst2 := mock.NewFakeCSQLInstance(
10511054
"my-project", "my-region", "my-instance2",
1055+
mock.WithDNS("update.example.com"),
10521056
)
10531057
r := &changingResolver{
10541058
stage: new(int32),
@@ -1104,42 +1108,85 @@ func TestDialerUpdatesAutomaticallyAfterDnsChange(t *testing.T) {
11041108

11051109
func TestDialerChecksSubjectAlternativeNameAndSucceeds(t *testing.T) {
11061110

1107-
// Create an instance with custom SAN 'db.example.com'
1108-
inst := mock.NewFakeCSQLInstanceWithSan(
1109-
"my-project", "my-region", "my-instance", []string{"db.example.com"},
1110-
mock.WithDNS("db.example.com"),
1111-
mock.WithServerCAMode("GOOGLE_MANAGED_CAS_CA"),
1112-
)
1111+
tcs := []struct {
1112+
name string
1113+
legacy bool
1114+
icn string
1115+
dn string
1116+
}{{
1117+
name: "domainName DnsName older",
1118+
legacy: true,
1119+
icn: "my-project:my-region:my-instance",
1120+
}, {
1121+
name: "domainName DnsNames newer",
1122+
legacy: false,
1123+
icn: "my-project:my-region:my-instance",
1124+
},
1125+
{
1126+
name: "InstanceConnectionName DnsName older",
1127+
legacy: true,
1128+
icn: "my-project:my-region:my-instance",
1129+
dn: "db.example.com",
1130+
}, {
1131+
name: "InstanceConnectionName DnsNames newer",
1132+
legacy: false,
1133+
icn: "my-project:my-region:my-instance",
1134+
dn: "db.example.com",
1135+
}}
1136+
for _, tc := range tcs {
1137+
t.Run(tc.name, func(t *testing.T) {
1138+
// Create an instance with custom SAN 'db.example.com'
1139+
var inst mock.FakeCSQLInstance
1140+
if tc.legacy || tc.dn == "" {
1141+
inst = mock.NewFakeCSQLInstance(
1142+
"my-project", "my-region", "my-instance",
1143+
mock.WithDNS("db.example.com"),
1144+
mock.WithServerCAMode("GOOGLE_MANAGED_CAS_CA"),
1145+
)
1146+
} else {
1147+
inst = mock.NewFakeCSQLInstance(
1148+
"my-project", "my-region", "my-instance",
1149+
mock.WithDNSMapping("db.example.com", "INSTANCE", "CUSTOM_SAN"),
1150+
mock.WithServerCAMode("GOOGLE_MANAGED_CAS_CA"),
1151+
)
1152+
}
11131153

1114-
wantName, _ := instance.ParseConnNameWithDomainName("my-project:my-region:my-instance", "db.example.com")
1115-
d := setupDialer(t, setupConfig{
1116-
testInstance: inst,
1117-
reqs: []*mock.Request{
1118-
mock.InstanceGetSuccess(inst, 1),
1119-
mock.CreateEphemeralSuccess(inst, 1),
1120-
},
1121-
dialerOptions: []Option{
1122-
WithTokenSource(mock.EmptyTokenSource{}),
1123-
WithResolver(&fakeResolver{
1124-
entries: map[string]instance.ConnName{
1125-
"db.example.com": wantName,
1154+
wantName, _ := instance.ParseConnNameWithDomainName(tc.icn, tc.dn)
1155+
d := setupDialer(t, setupConfig{
1156+
testInstance: inst,
1157+
reqs: []*mock.Request{
1158+
mock.InstanceGetSuccess(inst, 1),
1159+
mock.CreateEphemeralSuccess(inst, 1),
11261160
},
1127-
}),
1128-
},
1129-
})
1161+
dialerOptions: []Option{
1162+
WithTokenSource(mock.EmptyTokenSource{}),
1163+
WithResolver(&fakeResolver{
1164+
entries: map[string]instance.ConnName{
1165+
"db.example.com": wantName,
1166+
"my-project:my-region:my-instance": wantName,
1167+
},
1168+
}),
1169+
},
1170+
})
1171+
dnOrIcn := tc.icn
1172+
if tc.dn != "" {
1173+
dnOrIcn = tc.dn
1174+
}
11301175

1131-
// Dial db.example.com
1132-
testSuccessfulDial(
1133-
context.Background(), t, d,
1134-
"db.example.com",
1135-
)
1176+
// Dial db.example.com
1177+
testSuccessfulDial(
1178+
context.Background(), t, d,
1179+
dnOrIcn,
1180+
)
1181+
})
1182+
}
11361183
}
11371184

11381185
func TestDialerChecksSubjectAlternativeNameAndFails(t *testing.T) {
11391186

11401187
// Create an instance with custom SAN 'db.example.com'
1141-
inst := mock.NewFakeCSQLInstanceWithSan(
1142-
"my-project", "my-region", "my-instance", []string{"db.example.com"},
1188+
inst := mock.NewFakeCSQLInstance(
1189+
"my-project", "my-region", "my-instance",
11431190
mock.WithDNS("db.example.com"),
11441191
mock.WithServerCAMode("GOOGLE_MANAGED_CAS_CA"),
11451192
)
@@ -1207,8 +1254,8 @@ func TestDialerRefreshesAfterRotateCACerts(t *testing.T) {
12071254
}
12081255
for _, tc := range tcs {
12091256
t.Run(tc.desc, func(t *testing.T) {
1210-
inst := mock.NewFakeCSQLInstanceWithSan(
1211-
"my-project", "my-region", "my-instance", []string{"db.example.com"},
1257+
inst := mock.NewFakeCSQLInstance(
1258+
"my-project", "my-region", "my-instance",
12121259
mock.WithDNS("db.example.com"),
12131260
mock.WithServerCAMode("GOOGLE_MANAGED_CAS_CA"),
12141261
)

e2e_mysql_test.go

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,12 @@
1515
package cloudsqlconn_test
1616

1717
import (
18-
"context"
1918
"database/sql"
20-
"fmt"
2119
"os"
2220
"testing"
2321
"time"
2422

2523
"cloud.google.com/go/cloudsqlconn"
26-
"cloud.google.com/go/cloudsqlconn/instance"
2724
"cloud.google.com/go/cloudsqlconn/mysql/mysql"
2825
gomysql "github.com/go-sql-driver/mysql"
2926
)
@@ -55,16 +52,6 @@ func requireMySQLVars(t *testing.T) {
5552
}
5653
}
5754

58-
type mockResolver struct {
59-
}
60-
61-
func (r *mockResolver) Resolve(_ context.Context, name string) (instanceName instance.ConnName, err error) {
62-
if name == "mysql.example.com" {
63-
return instance.ParseConnNameWithDomainName(mysqlConnName, "mysql.example.com")
64-
}
65-
return instance.ConnName{}, fmt.Errorf("no resolution for %v", name)
66-
}
67-
6855
func TestMySQLDriver(t *testing.T) {
6956
if testing.Short() {
7057
t.Skip("skipping MySQL integration tests")
@@ -94,14 +81,6 @@ func TestMySQLDriver(t *testing.T) {
9481
user: mysqlIAMUser,
9582
password: "password",
9683
},
97-
{
98-
desc: "with dns",
99-
driverName: "cloudsql-mysql-dns",
100-
opts: []cloudsqlconn.Option{cloudsqlconn.WithResolver(&mockResolver{})},
101-
instanceName: "mysql.example.com",
102-
user: mysqlUser,
103-
password: mysqlPass,
104-
},
10584
}
10685

10786
for _, tc := range tcs {

go.mod

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ module cloud.google.com/go/cloudsqlconn
22

33
go 1.23.0
44

5-
toolchain go1.23.7
6-
75
require (
86
cloud.google.com/go/auth v0.15.0
97
cloud.google.com/go/auth/oauth2adapt v0.2.7
@@ -16,8 +14,8 @@ require (
1614
golang.org/x/net v0.37.0
1715
golang.org/x/oauth2 v0.28.0
1816
golang.org/x/time v0.11.0
19-
google.golang.org/api v0.224.0
20-
google.golang.org/genproto/googleapis/rpc v0.0.0-20250227231956-55c901821b1e
17+
google.golang.org/api v0.225.0
18+
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb
2119
google.golang.org/grpc v1.71.0
2220
)
2321

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -305,17 +305,17 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
305305
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
306306
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
307307
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
308-
google.golang.org/api v0.224.0 h1:Ir4UPtDsNiwIOHdExr3fAj4xZ42QjK7uQte3lORLJwU=
309-
google.golang.org/api v0.224.0/go.mod h1:3V39my2xAGkodXy0vEqcEtkqgw2GtrFL5WuBZlCTCOQ=
308+
google.golang.org/api v0.225.0 h1:+4/IVqBQm0MV5S+JW3kdEGC1WtOmM2mXN1LKH1LdNlw=
309+
google.golang.org/api v0.225.0/go.mod h1:WP/0Xm4LVvMOCldfvOISnWquSRWbG2kArDZcg+W2DbY=
310310
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
311311
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
312312
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
313313
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
314314
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
315315
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24=
316316
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=
317-
google.golang.org/genproto/googleapis/rpc v0.0.0-20250227231956-55c901821b1e h1:YA5lmSs3zc/5w+xsRcHqpETkaYyK63ivEPzNTcUUlSA=
318-
google.golang.org/genproto/googleapis/rpc v0.0.0-20250227231956-55c901821b1e/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
317+
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
318+
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
319319
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
320320
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
321321
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=

internal/cloudsql/instance.go

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -242,43 +242,44 @@ func (c ConnectionInfo) TLSConfig() *tls.Config {
242242
pool.AddCert(caCert)
243243
}
244244

245-
// For CAS instances, we can rely on the DNS name to verify the server identity.
246-
if c.ServerCAMode != "" && c.ServerCAMode != "GOOGLE_MANAGED_INTERNAL_CA" {
247-
// By default, use Standard TLS hostname verification name to
248-
// verify the server identity.
249-
250-
// If the connector was configured with a domain name, use that domain name
251-
// to validate the certificate. Otherwise, use the DNS name from the
252-
// instance ConnectionInfo API response.
253-
serverName := c.ConnectionName.DomainName()
254-
if serverName == "" {
255-
serverName = c.DNSName
256-
}
257-
245+
// If the instance metadata does not contain a domain name, use the legacy
246+
// validation checking the CN field for the instance connection name.
247+
if c.DNSName == "" {
258248
return &tls.Config{
259-
ServerName: serverName,
249+
ServerName: c.ConnectionName.String(),
260250
Certificates: []tls.Certificate{c.ClientCertificate},
261251
RootCAs: pool,
262-
MinVersion: tls.VersionTLS13,
252+
// We need to set InsecureSkipVerify to true due to
253+
// https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/194
254+
// https://tip.golang.org/doc/go1.11#crypto/x509
255+
//
256+
// Since we have a secure channel to the Cloud SQL API which we use to
257+
// retrieve the certificates, we instead need to implement our own
258+
// VerifyPeerCertificate function that will verify that the certificate
259+
// is OK.
260+
InsecureSkipVerify: true,
261+
VerifyPeerCertificate: verifyPeerCertificateFunc(c.ConnectionName, pool),
262+
MinVersion: tls.VersionTLS13,
263263
}
264264
}
265-
// For legacy instances use the custom TLS validation
265+
266+
// If the connector was configured with a domain name, use that domain name
267+
// to validate the certificate. Otherwise, use the DNS name from the
268+
// instance metadata retrieved from the ConnectSettings API endpoint.
269+
serverName := c.ConnectionName.DomainName()
270+
if serverName == "" {
271+
serverName = c.DNSName
272+
}
273+
274+
// By default, use Standard TLS hostname verification name to
275+
// verify the server identity.
266276
return &tls.Config{
267-
ServerName: c.ConnectionName.String(),
277+
ServerName: serverName,
268278
Certificates: []tls.Certificate{c.ClientCertificate},
269279
RootCAs: pool,
270-
// We need to set InsecureSkipVerify to true due to
271-
// https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/194
272-
// https://tip.golang.org/doc/go1.11#crypto/x509
273-
//
274-
// Since we have a secure channel to the Cloud SQL API which we use to
275-
// retrieve the certificates, we instead need to implement our own
276-
// VerifyPeerCertificate function that will verify that the certificate
277-
// is OK.
278-
InsecureSkipVerify: true,
279-
VerifyPeerCertificate: verifyPeerCertificateFunc(c.ConnectionName, pool),
280-
MinVersion: tls.VersionTLS13,
280+
MinVersion: tls.VersionTLS13,
281281
}
282+
282283
}
283284

284285
// verifyPeerCertificateFunc creates a VerifyPeerCertificate func that

internal/cloudsql/refresh.go

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,26 @@ func fetchMetadata(
100100

101101
// resolve DnsName into IP address for PSC
102102
// Note that we have to check for PSC enablement first because CAS instances also set the DnsName.
103-
if db.PscEnabled && db.DnsName != "" {
104-
ipAddrs[PSC] = db.DnsName
103+
if db.PscEnabled {
104+
// Search the dns_names field for the PSC DNS Name.
105+
pscDNSName := ""
106+
for _, dnm := range db.DnsNames {
107+
if dnm.Name != "" &&
108+
dnm.ConnectionType == "PRIVATE_SERVICE_CONNECT" && dnm.DnsScope == "INSTANCE" {
109+
pscDNSName = dnm.Name
110+
break
111+
}
112+
}
113+
114+
// If the psc dns name was not found, use the legacy dns_name field
115+
if pscDNSName == "" && db.DnsName != "" {
116+
pscDNSName = db.DnsName
117+
}
118+
119+
// If the psc dns name was found, add it to the ipaddrs map.
120+
if pscDNSName != "" {
121+
ipAddrs[PSC] = pscDNSName
122+
}
105123
}
106124

107125
if len(ipAddrs) == 0 {
@@ -128,11 +146,22 @@ func fetchMetadata(
128146
caCerts = append(caCerts, caCert)
129147
}
130148

149+
// Find a DNS name to use to validate the certificate from the dns_names field. Any
150+
// name in the list may be used to validate the server TLS certificate.
151+
// Fall back to legacy dns_name field if necessary.
152+
var serverName string
153+
if len(db.DnsNames) > 0 {
154+
serverName = db.DnsNames[0].Name
155+
}
156+
if serverName == "" {
157+
serverName = db.DnsName
158+
}
159+
131160
m = metadata{
132161
ipAddrs: ipAddrs,
133162
serverCACert: caCerts,
134163
version: db.DatabaseVersion,
135-
dnsName: db.DnsName,
164+
dnsName: serverName,
136165
serverCAMode: db.ServerCaMode,
137166
}
138167

internal/mock/certs.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,9 +246,9 @@ func (ct *TLSCertificates) generateServerCertWithCn(cn string) *x509.Certificate
246246
// serverChain creates a []tls.Certificate for use with a TLS server socket.
247247
// serverCAMode controls whether this returns a legacy or CAS server
248248
// certificate.
249-
func (ct *TLSCertificates) serverChain(serverCAMode string) []tls.Certificate {
249+
func (ct *TLSCertificates) serverChain(useStandardTLSValidation bool) []tls.Certificate {
250250
// if this server is running in legacy mode
251-
if serverCAMode == "" || serverCAMode == "GOOGLE_MANAGED_INTERNAL_CA" {
251+
if !useStandardTLSValidation {
252252
return []tls.Certificate{{
253253
Certificate: [][]byte{ct.serverCert.Raw, ct.serverCaCert.Raw},
254254
PrivateKey: ct.serverKey,

0 commit comments

Comments
 (0)