Skip to content
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

feat(spanner): mTLS setup for spanner external host clients #3574

Merged
merged 7 commits into from
Jan 8, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -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."),
Expand Down Expand Up @@ -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 <code>null</code>, then the
* default JDBC user agent string will be used.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -192,6 +196,20 @@ public class ConnectionProperties {
BooleanConverter.INSTANCE,
Context.STARTUP);

static final ConnectionProperty<String> 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<String> 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<String> CREDENTIALS_URL =
create(
CREDENTIALS_PROPERTY_NAME,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
sagnghos marked this conversation as resolved.
Show resolved Hide resolved

@VisibleForTesting
static SpannerPoolKey of(ConnectionOptions options) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -393,6 +397,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);
}
Expand Down
Loading