Skip to content

Commit d14b4e4

Browse files
authoredJan 10, 2025··
feat: Support Customer Managed Private CA for server certificates. (#2095)
Support instances configured with a Customer managed CAS private CA. This includes the integration test.
1 parent 595b022 commit d14b4e4

File tree

4 files changed

+128
-2
lines changed

4 files changed

+128
-2
lines changed
 

‎.github/workflows/tests.yml

+8
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ jobs:
140140
POSTGRES_DB:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_DB
141141
POSTGRES_CAS_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CAS_CONNECTION_NAME
142142
POSTGRES_CAS_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CAS_PASS
143+
POSTGRES_CUSTOMER_CAS_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_CONNECTION_NAME
144+
POSTGRES_CUSTOMER_CAS_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_PASS
143145
SQLSERVER_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_CONNECTION_NAME
144146
SQLSERVER_USER:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_USER
145147
SQLSERVER_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_PASS
@@ -162,6 +164,8 @@ jobs:
162164
POSTGRES_DB: "${{ steps.secrets.outputs.POSTGRES_DB }}"
163165
POSTGRES_CAS_CONNECTION_NAME: "${{ steps.secrets.outputs.POSTGRES_CAS_CONNECTION_NAME }}"
164166
POSTGRES_CAS_PASS: "${{ steps.secrets.outputs.POSTGRES_CAS_PASS }}"
167+
POSTGRES_CUSTOMER_CAS_CONNECTION_NAME: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_CONNECTION_NAME }}"
168+
POSTGRES_CUSTOMER_CAS_PASS: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_PASS }}"
165169
SQLSERVER_CONNECTION_NAME: "${{ steps.secrets.outputs.SQLSERVER_CONNECTION_NAME }}"
166170
SQLSERVER_USER: "${{ steps.secrets.outputs.SQLSERVER_USER }}"
167171
SQLSERVER_PASS: "${{ steps.secrets.outputs.SQLSERVER_PASS }}"
@@ -243,6 +247,8 @@ jobs:
243247
POSTGRES_DB:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_DB
244248
POSTGRES_CAS_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CAS_CONNECTION_NAME
245249
POSTGRES_CAS_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CAS_PASS
250+
POSTGRES_CUSTOMER_CAS_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_CONNECTION_NAME
251+
POSTGRES_CUSTOMER_CAS_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_PASS
246252
SQLSERVER_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_CONNECTION_NAME
247253
SQLSERVER_USER:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_USER
248254
SQLSERVER_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_PASS
@@ -266,6 +272,8 @@ jobs:
266272
POSTGRES_DB: "${{ steps.secrets.outputs.POSTGRES_DB }}"
267273
POSTGRES_CAS_CONNECTION_NAME: "${{ steps.secrets.outputs.POSTGRES_CAS_CONNECTION_NAME }}"
268274
POSTGRES_CAS_PASS: "${{ steps.secrets.outputs.POSTGRES_CAS_PASS }}"
275+
POSTGRES_CUSTOMER_CAS_CONNECTION_NAME: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_CONNECTION_NAME }}"
276+
POSTGRES_CUSTOMER_CAS_PASS: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_PASS }}"
269277
SQLSERVER_CONNECTION_NAME: "${{ steps.secrets.outputs.SQLSERVER_CONNECTION_NAME }}"
270278
SQLSERVER_USER: "${{ steps.secrets.outputs.SQLSERVER_USER }}"
271279
SQLSERVER_PASS: "${{ steps.secrets.outputs.SQLSERVER_PASS }}"

‎core/src/main/java/com/google/cloud/sql/core/DefaultConnectionInfoRepository.java

+17-1
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ private InstanceMetadata fetchMetadata(CloudSqlInstanceName instanceName, AuthTy
312312
instanceName,
313313
ipAddrs,
314314
instanceCaCertificates,
315-
"GOOGLE_MANAGED_CAS_CA".equals(instanceMetadata.getServerCaMode()),
315+
isCasManagedCertificate(instanceMetadata.getServerCaMode()),
316316
instanceMetadata.getDnsName(),
317317
pscEnabled);
318318
} catch (CertificateException ex) {
@@ -332,6 +332,22 @@ private InstanceMetadata fetchMetadata(CloudSqlInstanceName instanceName, AuthTy
332332
}
333333
}
334334

335+
/**
336+
* Instances with serverCaMode == GOOGLE_MANAGED_INTERNAL_CA or serverCaMode is null or empty use
337+
* a legacy, non-standard server certificate validation strategy. In all other cases, use standard
338+
* TLS hostname validation using the SubjectAlternativeNames records.
339+
*
340+
* @param serverCaMode from the instance metadata.
341+
* @return true when the instance uses a CAS certificate, and should use standard validation.
342+
*/
343+
private static boolean isCasManagedCertificate(String serverCaMode) {
344+
boolean useLegacyValidation =
345+
serverCaMode == null
346+
|| serverCaMode.isEmpty()
347+
|| "GOOGLE_MANAGED_INTERNAL_CA".equals(serverCaMode);
348+
return !useLegacyValidation;
349+
}
350+
335351
/**
336352
* Uses the Cloud SQL Admin API to create an ephemeral SSL certificate that is authenticated to
337353
* connect the Cloud SQL instance for up to 60 minutes.

‎core/src/test/java/com/google/cloud/sql/core/CloudSqlCoreTestingBase.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,8 @@ public LowLevelHttpResponse execute() throws IOException {
181181
.setRegion("myRegion")
182182
.setPscEnabled(psc ? Boolean.TRUE : null)
183183
.setDnsName(cas || psc ? "db.example.com" : null)
184-
.setServerCaMode(cas ? "GOOGLE_MANAGED_CAS_CA" : null);
184+
.setServerCaMode(
185+
cas ? "GOOGLE_MANAGED_CAS_CA" : "GOOGLE_MANAGED_INTERNAL_CA");
185186
settings.setFactory(jsonFactory);
186187
response
187188
.setContent(settings.toPrettyString())
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.sql.postgres;
18+
19+
import static com.google.common.truth.Truth.assertThat;
20+
import static com.google.common.truth.Truth.assertWithMessage;
21+
22+
import com.google.common.collect.ImmutableList;
23+
import com.zaxxer.hikari.HikariConfig;
24+
import com.zaxxer.hikari.HikariDataSource;
25+
import java.sql.*;
26+
import java.util.ArrayList;
27+
import java.util.List;
28+
import java.util.Properties;
29+
import java.util.concurrent.TimeUnit;
30+
import org.junit.Before;
31+
import org.junit.BeforeClass;
32+
import org.junit.Rule;
33+
import org.junit.Test;
34+
import org.junit.rules.Timeout;
35+
import org.junit.runner.RunWith;
36+
import org.junit.runners.JUnit4;
37+
38+
@RunWith(JUnit4.class)
39+
public class JdbcPostgresCustomerCasIntegrationTests {
40+
41+
private static final String CONNECTION_NAME =
42+
System.getenv("POSTGRES_CUSTOMER_CAS_CONNECTION_NAME");
43+
private static final String DB_NAME = System.getenv("POSTGRES_DB");
44+
private static final String DB_USER = System.getenv("POSTGRES_USER");
45+
private static final String DB_PASSWORD = System.getenv("POSTGRES_CUSTOMER_CAS_PASS");
46+
private static final ImmutableList<String> requiredEnvVars =
47+
ImmutableList.of(
48+
"POSTGRES_USER",
49+
"POSTGRES_CUSTOMER_CAS_PASS",
50+
"POSTGRES_DB",
51+
"POSTGRES_CUSTOMER_CAS_CONNECTION_NAME");
52+
@Rule public Timeout globalTimeout = new Timeout(80, TimeUnit.SECONDS);
53+
54+
private HikariDataSource connectionPool;
55+
56+
@BeforeClass
57+
public static void checkEnvVars() {
58+
// Check that required env vars are set
59+
requiredEnvVars.forEach(
60+
(varName) ->
61+
assertWithMessage(
62+
String.format(
63+
"Environment variable '%s' must be set to perform these tests.", varName))
64+
.that(System.getenv(varName))
65+
.isNotEmpty());
66+
}
67+
68+
@Before
69+
public void setUpPool() throws SQLException {
70+
// Set up URL parameters
71+
String jdbcURL = String.format("jdbc:postgresql:///%s", DB_NAME);
72+
Properties connProps = new Properties();
73+
connProps.setProperty("user", DB_USER);
74+
connProps.setProperty("password", DB_PASSWORD);
75+
connProps.setProperty("socketFactory", "com.google.cloud.sql.postgres.SocketFactory");
76+
connProps.setProperty("cloudSqlInstance", CONNECTION_NAME);
77+
78+
// Initialize connection pool
79+
HikariConfig config = new HikariConfig();
80+
config.setJdbcUrl(jdbcURL);
81+
config.setDataSourceProperties(connProps);
82+
config.setConnectionTimeout(10000); // 10s
83+
84+
this.connectionPool = new HikariDataSource(config);
85+
}
86+
87+
@Test
88+
public void pooledConnectionTest() throws SQLException {
89+
90+
List<Timestamp> rows = new ArrayList<>();
91+
try (Connection conn = connectionPool.getConnection()) {
92+
try (PreparedStatement selectStmt = conn.prepareStatement("SELECT NOW() as TS")) {
93+
ResultSet rs = selectStmt.executeQuery();
94+
while (rs.next()) {
95+
rows.add(rs.getTimestamp("TS"));
96+
}
97+
}
98+
}
99+
assertThat(rows.size()).isEqualTo(1);
100+
}
101+
}

0 commit comments

Comments
 (0)
Please sign in to comment.