Skip to content

feat: Support Customer CAS Private CA for server certificates. #2095

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ jobs:
POSTGRES_DB:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_DB
POSTGRES_CAS_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CAS_CONNECTION_NAME
POSTGRES_CAS_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CAS_PASS
POSTGRES_CUSTOMER_CAS_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_CONNECTION_NAME
POSTGRES_CUSTOMER_CAS_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_PASS
SQLSERVER_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_CONNECTION_NAME
SQLSERVER_USER:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_USER
SQLSERVER_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_PASS
Expand All @@ -162,6 +164,8 @@ jobs:
POSTGRES_DB: "${{ steps.secrets.outputs.POSTGRES_DB }}"
POSTGRES_CAS_CONNECTION_NAME: "${{ steps.secrets.outputs.POSTGRES_CAS_CONNECTION_NAME }}"
POSTGRES_CAS_PASS: "${{ steps.secrets.outputs.POSTGRES_CAS_PASS }}"
POSTGRES_CUSTOMER_CAS_CONNECTION_NAME: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_CONNECTION_NAME }}"
POSTGRES_CUSTOMER_CAS_PASS: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_PASS }}"
SQLSERVER_CONNECTION_NAME: "${{ steps.secrets.outputs.SQLSERVER_CONNECTION_NAME }}"
SQLSERVER_USER: "${{ steps.secrets.outputs.SQLSERVER_USER }}"
SQLSERVER_PASS: "${{ steps.secrets.outputs.SQLSERVER_PASS }}"
Expand Down Expand Up @@ -243,6 +247,8 @@ jobs:
POSTGRES_DB:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_DB
POSTGRES_CAS_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CAS_CONNECTION_NAME
POSTGRES_CAS_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CAS_PASS
POSTGRES_CUSTOMER_CAS_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_CONNECTION_NAME
POSTGRES_CUSTOMER_CAS_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_PASS
SQLSERVER_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_CONNECTION_NAME
SQLSERVER_USER:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_USER
SQLSERVER_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_PASS
Expand All @@ -266,6 +272,8 @@ jobs:
POSTGRES_DB: "${{ steps.secrets.outputs.POSTGRES_DB }}"
POSTGRES_CAS_CONNECTION_NAME: "${{ steps.secrets.outputs.POSTGRES_CAS_CONNECTION_NAME }}"
POSTGRES_CAS_PASS: "${{ steps.secrets.outputs.POSTGRES_CAS_PASS }}"
POSTGRES_CUSTOMER_CAS_CONNECTION_NAME: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_CONNECTION_NAME }}"
POSTGRES_CUSTOMER_CAS_PASS: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_PASS }}"
SQLSERVER_CONNECTION_NAME: "${{ steps.secrets.outputs.SQLSERVER_CONNECTION_NAME }}"
SQLSERVER_USER: "${{ steps.secrets.outputs.SQLSERVER_USER }}"
SQLSERVER_PASS: "${{ steps.secrets.outputs.SQLSERVER_PASS }}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ private InstanceMetadata fetchMetadata(CloudSqlInstanceName instanceName, AuthTy
instanceName,
ipAddrs,
instanceCaCertificates,
"GOOGLE_MANAGED_CAS_CA".equals(instanceMetadata.getServerCaMode()),
isCasManagedCertificate(instanceMetadata.getServerCaMode()),
instanceMetadata.getDnsName(),
pscEnabled);
} catch (CertificateException ex) {
Expand All @@ -332,6 +332,22 @@ private InstanceMetadata fetchMetadata(CloudSqlInstanceName instanceName, AuthTy
}
}

/**
* Instances with serverCaMode == GOOGLE_MANAGED_INTERNAL_CA or serverCaMode is null or empty use
* a legacy, non-standard server certificate validation strategy. In all other cases, use standard
* TLS hostname validation using the SubjectAlternativeNames records.
*
* @param serverCaMode from the instance metadata.
* @return true when the instance uses a CAS certificate, and should use standard validation.
*/
private static boolean isCasManagedCertificate(String serverCaMode) {
boolean useLegacyValidation =
serverCaMode == null
|| serverCaMode.isEmpty()
|| "GOOGLE_MANAGED_INTERNAL_CA".equals(serverCaMode);
return !useLegacyValidation;
}

/**
* Uses the Cloud SQL Admin API to create an ephemeral SSL certificate that is authenticated to
* connect the Cloud SQL instance for up to 60 minutes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,8 @@ public LowLevelHttpResponse execute() throws IOException {
.setRegion("myRegion")
.setPscEnabled(psc ? Boolean.TRUE : null)
.setDnsName(cas || psc ? "db.example.com" : null)
.setServerCaMode(cas ? "GOOGLE_MANAGED_CAS_CA" : null);
.setServerCaMode(
cas ? "GOOGLE_MANAGED_CAS_CA" : "GOOGLE_MANAGED_INTERNAL_CA");
settings.setFactory(jsonFactory);
response
.setContent(settings.toPrettyString())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.sql.postgres;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;

import com.google.common.collect.ImmutableList;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Timeout;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class JdbcPostgresCustomerCasIntegrationTests {

private static final String CONNECTION_NAME =
System.getenv("POSTGRES_CUSTOMER_CAS_CONNECTION_NAME");
private static final String DB_NAME = System.getenv("POSTGRES_DB");
private static final String DB_USER = System.getenv("POSTGRES_USER");
private static final String DB_PASSWORD = System.getenv("POSTGRES_CUSTOMER_CAS_PASS");
private static final ImmutableList<String> requiredEnvVars =
ImmutableList.of(
"POSTGRES_USER",
"POSTGRES_CUSTOMER_CAS_PASS",
"POSTGRES_DB",
"POSTGRES_CUSTOMER_CAS_CONNECTION_NAME");
@Rule public Timeout globalTimeout = new Timeout(80, TimeUnit.SECONDS);

private HikariDataSource connectionPool;

@BeforeClass
public static void checkEnvVars() {
// Check that required env vars are set
requiredEnvVars.forEach(
(varName) ->
assertWithMessage(
String.format(
"Environment variable '%s' must be set to perform these tests.", varName))
.that(System.getenv(varName))
.isNotEmpty());
}

@Before
public void setUpPool() throws SQLException {
// Set up URL parameters
String jdbcURL = String.format("jdbc:postgresql:///%s", DB_NAME);
Properties connProps = new Properties();
connProps.setProperty("user", DB_USER);
connProps.setProperty("password", DB_PASSWORD);
connProps.setProperty("socketFactory", "com.google.cloud.sql.postgres.SocketFactory");
connProps.setProperty("cloudSqlInstance", CONNECTION_NAME);

// Initialize connection pool
HikariConfig config = new HikariConfig();
config.setJdbcUrl(jdbcURL);
config.setDataSourceProperties(connProps);
config.setConnectionTimeout(10000); // 10s

this.connectionPool = new HikariDataSource(config);
}

@Test
public void pooledConnectionTest() throws SQLException {

List<Timestamp> rows = new ArrayList<>();
try (Connection conn = connectionPool.getConnection()) {
try (PreparedStatement selectStmt = conn.prepareStatement("SELECT NOW() as TS")) {
ResultSet rs = selectStmt.executeQuery();
while (rs.next()) {
rows.add(rs.getTimestamp("TS"));
}
}
}
assertThat(rows.size()).isEqualTo(1);
}
}
Loading