Skip to content

Commit bb6716b

Browse files
committed
feat(spanner): Support Dynamic Certificate/Key rotation in Spanner Omni
1 parent a4a47da commit bb6716b

12 files changed

Lines changed: 980 additions & 13 deletions

File tree

java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@
5353
import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStubSettings;
5454
import com.google.cloud.spanner.admin.instance.v1.InstanceAdminSettings;
5555
import com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStubSettings;
56+
import com.google.cloud.spanner.omni.DynamicKeyManager;
57+
import com.google.cloud.spanner.omni.DynamicTrustManager;
5658
import com.google.cloud.spanner.omni.SpannerOmniCredentials;
5759
import com.google.cloud.spanner.spi.SpannerRpcFactory;
5860
import com.google.cloud.spanner.spi.v1.ChannelEndpointCacheFactory;
@@ -85,6 +87,7 @@
8587
import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
8688
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
8789
import io.grpc.netty.shaded.io.netty.handler.ssl.SslContext;
90+
import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder;
8891
import io.opencensus.trace.Tracing;
8992
import io.opentelemetry.api.GlobalOpenTelemetry;
9093
import io.opentelemetry.api.OpenTelemetry;
@@ -941,14 +944,14 @@ protected SpannerOptions(Builder builder) {
941944
transportChannelExecutorThreadNameFormat = builder.transportChannelExecutorThreadNameFormat;
942945
channelProvider = builder.channelProvider;
943946
channelEndpointCacheFactory = builder.channelEndpointCacheFactory;
944-
if (builder.mTLSContext != null) {
947+
if (builder.omniSslContext != null) {
945948
channelConfigurator =
946949
channelBuilder -> {
947950
if (builder.channelConfigurator != null) {
948951
channelBuilder = builder.channelConfigurator.apply(channelBuilder);
949952
}
950953
if (channelBuilder instanceof NettyChannelBuilder) {
951-
((NettyChannelBuilder) channelBuilder).sslContext(builder.mTLSContext);
954+
((NettyChannelBuilder) channelBuilder).sslContext(builder.omniSslContext);
952955
}
953956
return channelBuilder;
954957
};
@@ -1292,6 +1295,13 @@ public GoogleCredentials getDefaultSpannerOmniCredentials() {
12921295
public static class Builder
12931296
extends ServiceOptions.Builder<Spanner, SpannerOptions, SpannerOptions.Builder> {
12941297
private static Builder prepareBuilder(Builder builder) {
1298+
if (builder.sslContextBuilder != null) {
1299+
try {
1300+
builder.omniSslContext = builder.sslContextBuilder.build();
1301+
} catch (Exception e) {
1302+
throw SpannerExceptionFactory.asSpannerException(e);
1303+
}
1304+
}
12951305
if (builder.instanceType == InstanceType.OMNI) {
12961306
builder.enableBuiltInMetrics = false;
12971307
builder.setProjectId(SPANNER_OMNI_PROJECT_ID);
@@ -1314,7 +1324,7 @@ private static Builder prepareBuilder(Builder builder) {
13141324
}
13151325
if (builder.credentials instanceof SpannerOmniCredentials) {
13161326
((SpannerOmniCredentials) builder.credentials)
1317-
.initChannel(builder.usePlainText, builder.mTLSContext);
1327+
.initChannel(builder.usePlainText, builder.omniSslContext);
13181328
}
13191329
} else {
13201330
if (builder.username != null || builder.secretBytes != null) {
@@ -1399,7 +1409,8 @@ private static Builder prepareBuilder(Builder builder) {
13991409
private MetricsProvider metricsProvider = DefaultMetricsProvider.INSTANCE;
14001410
private boolean enableLocationApi = SpannerOptions.environment.isEnableLocationApi();
14011411
private String monitoringHost = SpannerOptions.environment.getMonitoringHost();
1402-
private SslContext mTLSContext = null;
1412+
private SslContextBuilder sslContextBuilder = null;
1413+
private SslContext omniSslContext = null;
14031414
private boolean usePlainText = false;
14041415
private TransactionOptions defaultTransactionOptions = TransactionOptions.getDefaultInstance();
14051416
private RequestOptions.ClientContext clientContext;
@@ -2240,21 +2251,35 @@ public Builder setEmulatorHost(String emulatorHost) {
22402251

22412252
/**
22422253
* Configures mTLS authentication using the provided client certificate and key files. mTLS via
2243-
* useClientCert is only supported for Spanner Omni instances.
2254+
* useClientCert is only supported for Spanner Omni instances. Certificates and keys are loaded
2255+
* dynamically and reloaded automatically when rotated on disk.
22442256
*
22452257
* @param clientCertificate Path to the client certificate file.
22462258
* @param clientCertificateKey Path to the client private key file.
2247-
* @throws SpannerException If an error occurs while configuring the mTLS context
22482259
*/
22492260
public Builder useClientCert(String clientCertificate, String clientCertificateKey) {
2250-
try {
2251-
this.mTLSContext =
2252-
GrpcSslContexts.forClient()
2253-
.keyManager(new File(clientCertificate), new File(clientCertificateKey))
2254-
.build();
2255-
} catch (Exception e) {
2256-
throw SpannerExceptionFactory.asSpannerException(e);
2261+
Preconditions.checkNotNull(clientCertificate, "clientCertificate cannot be null");
2262+
Preconditions.checkNotNull(clientCertificateKey, "clientCertificateKey cannot be null");
2263+
if (this.sslContextBuilder == null) {
2264+
this.sslContextBuilder = GrpcSslContexts.forClient();
2265+
}
2266+
this.sslContextBuilder.keyManager(
2267+
new DynamicKeyManager(new File(clientCertificate), new File(clientCertificateKey)));
2268+
return this;
2269+
}
2270+
2271+
/**
2272+
* Configures the server root CA certificate for SSL/TLS authentication. The CA certificate is
2273+
* loaded dynamically and reloaded automatically when rotated on disk.
2274+
*
2275+
* @param caCertificate Path to the server root CA certificate file.
2276+
*/
2277+
public Builder setCaCertificate(String caCertificate) {
2278+
Preconditions.checkNotNull(caCertificate, "caCertificate cannot be null");
2279+
if (this.sslContextBuilder == null) {
2280+
this.sslContextBuilder = GrpcSslContexts.forClient();
22572281
}
2282+
this.sslContextBuilder.trustManager(new DynamicTrustManager(new File(caCertificate)));
22582283
return this;
22592284
}
22602285

java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static com.google.cloud.spanner.connection.ConnectionProperties.AUTOCOMMIT;
2020
import static com.google.cloud.spanner.connection.ConnectionProperties.AUTO_CONFIG_EMULATOR;
2121
import static com.google.cloud.spanner.connection.ConnectionProperties.AUTO_PARTITION_MODE;
22+
import static com.google.cloud.spanner.connection.ConnectionProperties.CA_CERTIFICATE;
2223
import static com.google.cloud.spanner.connection.ConnectionProperties.CHANNEL_PROVIDER;
2324
import static com.google.cloud.spanner.connection.ConnectionProperties.CLIENT_CERTIFICATE;
2425
import static com.google.cloud.spanner.connection.ConnectionProperties.CLIENT_KEY;
@@ -168,6 +169,7 @@ public class ConnectionOptions {
168169
static final String DEFAULT_CREDENTIALS = null;
169170
static final String DEFAULT_CLIENT_CERTIFICATE = null;
170171
static final String DEFAULT_CLIENT_KEY = null;
172+
static final String DEFAULT_CA_CERTIFICATE = null;
171173
static final String DEFAULT_OAUTH_TOKEN = null;
172174
static final Integer DEFAULT_MIN_SESSIONS = null;
173175
static final Integer DEFAULT_MAX_SESSIONS = null;
@@ -242,6 +244,9 @@ public class ConnectionOptions {
242244
/** Client key path to establish mTLS */
243245
static final String CLIENT_KEY_PROPERTY_NAME = "clientKey";
244246

247+
/** Server root CA certificate path for SSL/TLS */
248+
static final String CA_CERTIFICATE_PROPERTY_NAME = "caCertificate";
249+
245250
/** Name of the 'autocommit' connection property. */
246251
public static final String AUTOCOMMIT_PROPERTY_NAME = "autocommit";
247252

@@ -676,6 +681,21 @@ public Builder setType(SpannerOptions.InstanceType instanceType) {
676681
return this;
677682
}
678683

684+
public Builder setClientCertificate(String clientCertificate) {
685+
setConnectionPropertyValue(CLIENT_CERTIFICATE, clientCertificate);
686+
return this;
687+
}
688+
689+
public Builder setClientCertificateKey(String clientCertificateKey) {
690+
setConnectionPropertyValue(CLIENT_KEY, clientCertificateKey);
691+
return this;
692+
}
693+
694+
public Builder setCaCertificate(String caCertificate) {
695+
setConnectionPropertyValue(CA_CERTIFICATE, caCertificate);
696+
return this;
697+
}
698+
679699
/**
680700
* @return the {@link ConnectionOptions}
681701
*/
@@ -1300,6 +1320,10 @@ String getClientCertificateKey() {
13001320
return getInitialConnectionPropertyValue(CLIENT_KEY);
13011321
}
13021322

1323+
String getCaCertificate() {
1324+
return getInitialConnectionPropertyValue(CA_CERTIFICATE);
1325+
}
1326+
13031327
/**
13041328
* The (custom) user agent string to use for this connection. If <code>null</code>, then the
13051329
* default JDBC user agent string will be used.

java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import static com.google.cloud.spanner.connection.ConnectionOptions.AUTO_BATCH_DML_UPDATE_COUNT_VERIFICATION_PROPERTY_NAME;
2323
import static com.google.cloud.spanner.connection.ConnectionOptions.AUTO_PARTITION_MODE_PROPERTY_NAME;
2424
import static com.google.cloud.spanner.connection.ConnectionOptions.BATCH_DML_UPDATE_COUNT_PROPERTY_NAME;
25+
import static com.google.cloud.spanner.connection.ConnectionOptions.CA_CERTIFICATE_PROPERTY_NAME;
2526
import static com.google.cloud.spanner.connection.ConnectionOptions.CHANNEL_PROVIDER_PROPERTY_NAME;
2627
import static com.google.cloud.spanner.connection.ConnectionOptions.CLIENT_CERTIFICATE_PROPERTY_NAME;
2728
import static com.google.cloud.spanner.connection.ConnectionOptions.CLIENT_KEY_PROPERTY_NAME;
@@ -42,6 +43,7 @@
4243
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_AUTO_BATCH_DML_UPDATE_COUNT_VERIFICATION;
4344
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_AUTO_PARTITION_MODE;
4445
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_BATCH_DML_UPDATE_COUNT;
46+
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_CA_CERTIFICATE;
4547
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_CHANNEL_PROVIDER;
4648
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_CLIENT_CERTIFICATE;
4749
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_CLIENT_KEY;
@@ -329,6 +331,13 @@ public class ConnectionProperties {
329331
DEFAULT_CLIENT_KEY,
330332
StringValueConverter.INSTANCE,
331333
Context.STARTUP);
334+
static final ConnectionProperty<String> CA_CERTIFICATE =
335+
create(
336+
CA_CERTIFICATE_PROPERTY_NAME,
337+
"Specifies the file path to the server root CA certificate for SSL/TLS validation.",
338+
DEFAULT_CA_CERTIFICATE,
339+
StringValueConverter.INSTANCE,
340+
Context.STARTUP);
332341
static final ConnectionProperty<String> CREDENTIALS_URL =
333342
create(
334343
CREDENTIALS_PROPERTY_NAME,

java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ static class SpannerPoolKey {
176176
private final boolean enableEndToEndTracing;
177177
private final String clientCertificate;
178178
private final String clientCertificateKey;
179+
private final String caCertificate;
179180
private final SpannerOptions.InstanceType instanceType;
180181
private final Boolean enableDirectAccess;
181182
private final String universeDomain;
@@ -221,6 +222,7 @@ private SpannerPoolKey(ConnectionOptions options) throws IOException {
221222
this.enableEndToEndTracing = options.isEndToEndTracingEnabled();
222223
this.clientCertificate = options.getClientCertificate();
223224
this.clientCertificateKey = options.getClientCertificateKey();
225+
this.caCertificate = options.getCaCertificate();
224226
this.instanceType = options.getInstanceType();
225227
this.enableDirectAccess = options.isEnableDirectAccess();
226228
this.universeDomain = options.getUniverseDomain();
@@ -261,6 +263,7 @@ public boolean equals(Object o) {
261263
&& Objects.equals(this.enableEndToEndTracing, other.enableEndToEndTracing)
262264
&& Objects.equals(this.clientCertificate, other.clientCertificate)
263265
&& Objects.equals(this.clientCertificateKey, other.clientCertificateKey)
266+
&& Objects.equals(this.caCertificate, other.caCertificate)
264267
&& Objects.equals(this.instanceType, other.instanceType)
265268
&& Objects.equals(this.enableDirectAccess, other.enableDirectAccess)
266269
&& Objects.equals(this.universeDomain, other.universeDomain)
@@ -296,6 +299,7 @@ public int hashCode() {
296299
this.enableEndToEndTracing,
297300
this.clientCertificate,
298301
this.clientCertificateKey,
302+
this.caCertificate,
299303
this.instanceType,
300304
this.enableDirectAccess,
301305
this.universeDomain,
@@ -540,6 +544,9 @@ Spanner createSpanner(SpannerPoolKey key, ConnectionOptions options) {
540544
if (key.clientCertificate != null && key.clientCertificateKey != null) {
541545
builder.useClientCert(key.clientCertificate, key.clientCertificateKey);
542546
}
547+
if (key.caCertificate != null) {
548+
builder.setCaCertificate(key.caCertificate);
549+
}
543550
if (key.instanceType != null) {
544551
builder.setType(key.instanceType);
545552
}

0 commit comments

Comments
 (0)