diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java
index bc81b42903a..1d0c702cdfc 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java
@@ -71,9 +71,13 @@
import io.grpc.ExperimentalApi;
import io.grpc.ManagedChannelBuilder;
import io.grpc.MethodDescriptor;
+import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
+import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
+import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.Attributes;
+import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
@@ -952,6 +956,7 @@ public static class Builder
private boolean enableEndToEndTracing = SpannerOptions.environment.isEnableEndToEndTracing();
private boolean enableBuiltInMetrics = SpannerOptions.environment.isEnableBuiltInMetrics();
private String monitoringHost = SpannerOptions.environment.getMonitoringHost();
+ private SslContext mTLSContext = null;
private static String createCustomClientLibToken(String token) {
return token + " " + ServiceOptions.getGoogApiClientLibName();
@@ -1485,6 +1490,27 @@ public Builder setEmulatorHost(String emulatorHost) {
return this;
}
+ /**
+ * Configures mTLS authentication using the provided client certificate and key files. mTLS is
+ * only supported for external spanner hosts.
+ *
+ * @param clientCertificate Path to the client certificate file.
+ * @param clientCertificateKey Path to the client private key file.
+ * @throws SpannerException If an error occurs while configuring the mTLS context
+ */
+ @ExperimentalApi("https://github.com/googleapis/java-spanner/pull/3574")
+ public Builder useClientCert(String clientCertificate, String clientCertificateKey) {
+ try {
+ this.mTLSContext =
+ GrpcSslContexts.forClient()
+ .keyManager(new File(clientCertificate), new File(clientCertificateKey))
+ .build();
+ } catch (Exception e) {
+ throw SpannerExceptionFactory.asSpannerException(e);
+ }
+ return this;
+ }
+
/**
* Sets OpenTelemetry object to be used for Spanner Metrics and Traces. GlobalOpenTelemetry will
* be used as fallback if this options is not set.
@@ -1594,6 +1620,15 @@ public SpannerOptions build() {
// As we are using plain text, we should never send any credentials.
this.setCredentials(NoCredentials.getInstance());
}
+ if (mTLSContext != null) {
+ this.setChannelConfigurator(
+ builder -> {
+ if (builder instanceof NettyChannelBuilder) {
+ ((NettyChannelBuilder) builder).sslContext(mTLSContext);
+ }
+ return builder;
+ });
+ }
if (this.numChannels == null) {
this.numChannels =
this.grpcGcpExtensionEnabled ? GRPC_GCP_ENABLED_DEFAULT_CHANNELS : DEFAULT_CHANNELS;
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java
index 6e991816ab6..d205699bb9e 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java
@@ -20,6 +20,8 @@
import static com.google.cloud.spanner.connection.ConnectionProperties.AUTO_CONFIG_EMULATOR;
import static com.google.cloud.spanner.connection.ConnectionProperties.AUTO_PARTITION_MODE;
import static com.google.cloud.spanner.connection.ConnectionProperties.CHANNEL_PROVIDER;
+import static com.google.cloud.spanner.connection.ConnectionProperties.CLIENT_CERTIFICATE;
+import static com.google.cloud.spanner.connection.ConnectionProperties.CLIENT_KEY;
import static com.google.cloud.spanner.connection.ConnectionProperties.CREDENTIALS_PROVIDER;
import static com.google.cloud.spanner.connection.ConnectionProperties.CREDENTIALS_URL;
import static com.google.cloud.spanner.connection.ConnectionProperties.DATABASE_ROLE;
@@ -225,6 +227,8 @@ public String[] getValidValues() {
static final boolean DEFAULT_USE_VIRTUAL_THREADS = false;
static final boolean DEFAULT_USE_VIRTUAL_GRPC_TRANSPORT_THREADS = false;
static final String DEFAULT_CREDENTIALS = null;
+ static final String DEFAULT_CLIENT_CERTIFICATE = null;
+ static final String DEFAULT_CLIENT_KEY = null;
static final String DEFAULT_OAUTH_TOKEN = null;
static final Integer DEFAULT_MIN_SESSIONS = null;
static final Integer DEFAULT_MAX_SESSIONS = null;
@@ -263,6 +267,10 @@ public String[] getValidValues() {
private static final String DEFAULT_EMULATOR_HOST = "http://localhost:9010";
/** Use plain text is only for local testing purposes. */
static final String USE_PLAIN_TEXT_PROPERTY_NAME = "usePlainText";
+ /** Client certificate path to establish mTLS */
+ static final String CLIENT_CERTIFICATE_PROPERTY_NAME = "clientCertificate";
+ /** Client key path to establish mTLS */
+ static final String CLIENT_KEY_PROPERTY_NAME = "clientKey";
/** Name of the 'autocommit' connection property. */
public static final String AUTOCOMMIT_PROPERTY_NAME = "autocommit";
/** Name of the 'readonly' connection property. */
@@ -434,6 +442,12 @@ static boolean isEnableTransactionalConnectionStateForPostgreSQL() {
USE_PLAIN_TEXT_PROPERTY_NAME,
"Use a plain text communication channel (i.e. non-TLS) for communicating with the server (true/false). Set this value to true for communication with the Cloud Spanner emulator.",
DEFAULT_USE_PLAIN_TEXT),
+ ConnectionProperty.createStringProperty(
+ CLIENT_CERTIFICATE_PROPERTY_NAME,
+ "Specifies the file path to the client certificate required for establishing an mTLS connection."),
+ ConnectionProperty.createStringProperty(
+ CLIENT_KEY_PROPERTY_NAME,
+ "Specifies the file path to the client private key required for establishing an mTLS connection."),
ConnectionProperty.createStringProperty(
USER_AGENT_PROPERTY_NAME,
"The custom user-agent property name to use when communicating with Cloud Spanner. This property is intended for internal library usage, and should not be set by applications."),
@@ -1291,6 +1305,14 @@ boolean isUsePlainText() {
|| getInitialConnectionPropertyValue(USE_PLAIN_TEXT);
}
+ String getClientCertificate() {
+ return getInitialConnectionPropertyValue(CLIENT_CERTIFICATE);
+ }
+
+ String getClientCertificateKey() {
+ return getInitialConnectionPropertyValue(CLIENT_KEY);
+ }
+
/**
* The (custom) user agent string to use for this connection. If null, then the
* default JDBC user agent string will be used.
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java
index fd40efa8f4a..6f2628e5a04 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java
@@ -22,6 +22,8 @@
import static com.google.cloud.spanner.connection.ConnectionOptions.AUTO_BATCH_DML_UPDATE_COUNT_VERIFICATION_PROPERTY_NAME;
import static com.google.cloud.spanner.connection.ConnectionOptions.AUTO_PARTITION_MODE_PROPERTY_NAME;
import static com.google.cloud.spanner.connection.ConnectionOptions.CHANNEL_PROVIDER_PROPERTY_NAME;
+import static com.google.cloud.spanner.connection.ConnectionOptions.CLIENT_CERTIFICATE_PROPERTY_NAME;
+import static com.google.cloud.spanner.connection.ConnectionOptions.CLIENT_KEY_PROPERTY_NAME;
import static com.google.cloud.spanner.connection.ConnectionOptions.CREDENTIALS_PROPERTY_NAME;
import static com.google.cloud.spanner.connection.ConnectionOptions.CREDENTIALS_PROVIDER_PROPERTY_NAME;
import static com.google.cloud.spanner.connection.ConnectionOptions.DATABASE_ROLE_PROPERTY_NAME;
@@ -33,6 +35,8 @@
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_AUTO_BATCH_DML_UPDATE_COUNT_VERIFICATION;
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_AUTO_PARTITION_MODE;
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_CHANNEL_PROVIDER;
+import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_CLIENT_CERTIFICATE;
+import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_CLIENT_KEY;
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_CREDENTIALS;
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DATABASE_ROLE;
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_DATA_BOOST_ENABLED;
@@ -192,6 +196,20 @@ public class ConnectionProperties {
BooleanConverter.INSTANCE,
Context.STARTUP);
+ static final ConnectionProperty CLIENT_CERTIFICATE =
+ create(
+ CLIENT_CERTIFICATE_PROPERTY_NAME,
+ "Specifies the file path to the client certificate required for establishing an mTLS connection.",
+ DEFAULT_CLIENT_CERTIFICATE,
+ StringValueConverter.INSTANCE,
+ Context.STARTUP);
+ static final ConnectionProperty CLIENT_KEY =
+ create(
+ CLIENT_KEY_PROPERTY_NAME,
+ "Specifies the file path to the client private key required for establishing an mTLS connection.",
+ DEFAULT_CLIENT_KEY,
+ StringValueConverter.INSTANCE,
+ Context.STARTUP);
static final ConnectionProperty CREDENTIALS_URL =
create(
CREDENTIALS_PROPERTY_NAME,
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java
index 81246e41938..9558947156c 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java
@@ -161,6 +161,8 @@ static class SpannerPoolKey {
private final Boolean enableExtendedTracing;
private final Boolean enableApiTracing;
private final boolean enableEndToEndTracing;
+ private final String clientCertificate;
+ private final String clientCertificateKey;
@VisibleForTesting
static SpannerPoolKey of(ConnectionOptions options) {
@@ -192,6 +194,8 @@ private SpannerPoolKey(ConnectionOptions options) throws IOException {
this.enableExtendedTracing = options.isEnableExtendedTracing();
this.enableApiTracing = options.isEnableApiTracing();
this.enableEndToEndTracing = options.isEndToEndTracingEnabled();
+ this.clientCertificate = options.getClientCertificate();
+ this.clientCertificateKey = options.getClientCertificateKey();
}
@Override
@@ -214,7 +218,9 @@ public boolean equals(Object o) {
&& Objects.equals(this.openTelemetry, other.openTelemetry)
&& Objects.equals(this.enableExtendedTracing, other.enableExtendedTracing)
&& Objects.equals(this.enableApiTracing, other.enableApiTracing)
- && Objects.equals(this.enableEndToEndTracing, other.enableEndToEndTracing);
+ && Objects.equals(this.enableEndToEndTracing, other.enableEndToEndTracing)
+ && Objects.equals(this.clientCertificate, other.clientCertificate)
+ && Objects.equals(this.clientCertificateKey, other.clientCertificateKey);
}
@Override
@@ -233,7 +239,9 @@ public int hashCode() {
this.openTelemetry,
this.enableExtendedTracing,
this.enableApiTracing,
- this.enableEndToEndTracing);
+ this.enableEndToEndTracing,
+ this.clientCertificate,
+ this.clientCertificateKey);
}
}
@@ -393,6 +401,9 @@ Spanner createSpanner(SpannerPoolKey key, ConnectionOptions options) {
// Set a custom channel configurator to allow http instead of https.
builder.setChannelConfigurator(ManagedChannelBuilder::usePlaintext);
}
+ if (key.clientCertificate != null && key.clientCertificateKey != null) {
+ builder.useClientCert(key.clientCertificate, key.clientCertificateKey);
+ }
if (options.getConfigurator() != null) {
options.getConfigurator().configure(builder);
}