Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0fb7c7e
feat(spanner): Support Dynamic Certificate/Key rotation in Spanner Omni
sagnghos Sep 18, 2026
8473bb7
fix(spanner): Throttle key/trust manager file stat checks and use nat…
sagnghos Sep 21, 2026
c5ec704
fix(spanner): Detect PKCS#1 keys with actionable error and clean up t…
sagnghos Sep 21, 2026
a077de2
Address review comments: avoid capturing builder in SpannerOptions la…
sagnghos Sep 21, 2026
959f887
Address review comments: simplify reloadMaterial and suppress excepti…
sagnghos Sep 21, 2026
7fcbedf
docs(spanner): Add Javadocs for ConnectionOptions builder and Dynamic…
sagnghos Sep 21, 2026
6993ccf
fix(spanner): Validate private key matches certificate and optimize f…
sagnghos Sep 21, 2026
ab27a98
fix(spanner): Address review comments: versioned alias map, binary DE…
sagnghos Sep 21, 2026
f78415f
fix(spanner): Non-blocking Netty handshakes via background scheduled …
sagnghos Sep 21, 2026
53f59f9
fix(spanner): On-demand rate-limited checking on TLS handshake, remov…
sagnghos Sep 21, 2026
c1f0f39
fix(spanner): Use monotonic System.nanoTime() and ReentrantLock for t…
sagnghos Sep 21, 2026
c031825
Store certificate and key paths in SpannerOptions and Builder
sagnghos Sep 21, 2026
0b61f6e
Validate client certificate and key are both provided in prepareBuilder
sagnghos Sep 21, 2026
8e80a78
Use Strings.isNullOrEmpty for certificate and key validation
sagnghos Sep 21, 2026
0789fb9
Use lock.tryLock() in DynamicKeyManager and DynamicTrustManager to pr…
sagnghos Sep 21, 2026
c3ddcc7
Perform file attribute checks only after acquiring lock in DynamicKey…
sagnghos Sep 21, 2026
1886649
Use removeIf on materials.keySet() in DynamicKeyManager
sagnghos Sep 21, 2026
4da5bf9
Optimize parsePrivateKey and extractPemContent in DynamicKeyManager
sagnghos Sep 21, 2026
27d701f
Safely evict key material aliases in DynamicKeyManager
sagnghos Sep 21, 2026
cacf044
Safely handle null issuers in DynamicTrustManager and speed up tests …
sagnghos Sep 21, 2026
a376228
Use standard JKS keystore in DynamicTrustManager
sagnghos Sep 21, 2026
8d9c737
Use KeyStore.getDefaultType() and clone accepted issuers in DynamicTr…
sagnghos Sep 21, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStubSettings;
import com.google.cloud.spanner.admin.instance.v1.InstanceAdminSettings;
import com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStubSettings;
import com.google.cloud.spanner.omni.DynamicKeyManager;
import com.google.cloud.spanner.omni.DynamicTrustManager;
import com.google.cloud.spanner.omni.SpannerOmniCredentials;
import com.google.cloud.spanner.spi.SpannerRpcFactory;
import com.google.cloud.spanner.spi.v1.ChannelEndpointCacheFactory;
Expand Down Expand Up @@ -85,6 +87,7 @@
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.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder;
import io.opencensus.trace.Tracing;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
Expand Down Expand Up @@ -356,6 +359,9 @@ static GcpChannelPoolOptions mergeWithDefaultChannelPoolOptions(
private final boolean autoTaggingEnabled;
private final List<String> autoTaggingPackages;
private final int autoTaggingTracerLimit;
private final String clientCertificate;
private final String clientCertificateKey;
private final String caCertificate;

enum TracingFramework {
OPEN_CENSUS,
Expand Down Expand Up @@ -941,14 +947,21 @@ protected SpannerOptions(Builder builder) {
transportChannelExecutorThreadNameFormat = builder.transportChannelExecutorThreadNameFormat;
channelProvider = builder.channelProvider;
channelEndpointCacheFactory = builder.channelEndpointCacheFactory;
if (builder.mTLSContext != null) {
clientCertificate = builder.clientCertificate;
clientCertificateKey = builder.clientCertificateKey;
caCertificate = builder.caCertificate;
if (builder.omniSslContext != null) {
final SslContext sslContext = builder.omniSslContext;
@SuppressWarnings("rawtypes")
final ApiFunction<ManagedChannelBuilder, ManagedChannelBuilder> parentConfigurator =
builder.channelConfigurator;
channelConfigurator =
channelBuilder -> {
if (builder.channelConfigurator != null) {
channelBuilder = builder.channelConfigurator.apply(channelBuilder);
if (parentConfigurator != null) {
channelBuilder = parentConfigurator.apply(channelBuilder);
}
if (channelBuilder instanceof NettyChannelBuilder) {
((NettyChannelBuilder) channelBuilder).sslContext(builder.mTLSContext);
((NettyChannelBuilder) channelBuilder).sslContext(sslContext);
}
return channelBuilder;
};
Comment thread
sagnghos marked this conversation as resolved.
Expand Down Expand Up @@ -1292,6 +1305,31 @@ public GoogleCredentials getDefaultSpannerOmniCredentials() {
public static class Builder
extends ServiceOptions.Builder<Spanner, SpannerOptions, SpannerOptions.Builder> {
private static Builder prepareBuilder(Builder builder) {
boolean hasClientCert = !Strings.isNullOrEmpty(builder.clientCertificate);
boolean hasClientKey = !Strings.isNullOrEmpty(builder.clientCertificateKey);
boolean hasCaCert = !Strings.isNullOrEmpty(builder.caCertificate);

if (hasClientCert || hasClientKey || hasCaCert) {
if (hasClientCert != hasClientKey) {
throw new IllegalArgumentException(
"Both clientCertificate and clientCertificateKey must be provided together");
}
try {
SslContextBuilder sslContextBuilder = GrpcSslContexts.forClient();
if (hasClientCert) {
sslContextBuilder.keyManager(
new DynamicKeyManager(
new File(builder.clientCertificate), new File(builder.clientCertificateKey)));
}
if (hasCaCert) {
sslContextBuilder.trustManager(
new DynamicTrustManager(new File(builder.caCertificate)));
}
builder.omniSslContext = sslContextBuilder.build();
} catch (Exception e) {
throw SpannerExceptionFactory.asSpannerException(e);
}
}
if (builder.instanceType == InstanceType.OMNI) {
builder.enableBuiltInMetrics = false;
builder.setProjectId(SPANNER_OMNI_PROJECT_ID);
Expand All @@ -1314,7 +1352,7 @@ private static Builder prepareBuilder(Builder builder) {
}
if (builder.credentials instanceof SpannerOmniCredentials) {
((SpannerOmniCredentials) builder.credentials)
.initChannel(builder.usePlainText, builder.mTLSContext);
.initChannel(builder.usePlainText, builder.omniSslContext);
}
} else {
if (builder.username != null || builder.secretBytes != null) {
Expand Down Expand Up @@ -1399,7 +1437,10 @@ private static Builder prepareBuilder(Builder builder) {
private MetricsProvider metricsProvider = DefaultMetricsProvider.INSTANCE;
private boolean enableLocationApi = SpannerOptions.environment.isEnableLocationApi();
private String monitoringHost = SpannerOptions.environment.getMonitoringHost();
private SslContext mTLSContext = null;
private String clientCertificate = null;
private String clientCertificateKey = null;
private String caCertificate = null;
private SslContext omniSslContext = null;
private boolean usePlainText = false;
private TransactionOptions defaultTransactionOptions = TransactionOptions.getDefaultInstance();
private RequestOptions.ClientContext clientContext;
Expand Down Expand Up @@ -1517,6 +1558,9 @@ protected Builder() {
this.autoTaggingEnabled = options.autoTaggingEnabled;
this.autoTaggingPackages = options.autoTaggingPackages;
this.autoTaggingTracerLimit = options.autoTaggingTracerLimit;
this.clientCertificate = options.clientCertificate;
this.clientCertificateKey = options.clientCertificateKey;
this.caCertificate = options.caCertificate;
}

@Override
Expand Down Expand Up @@ -2240,21 +2284,33 @@ public Builder setEmulatorHost(String emulatorHost) {

/**
* Configures mTLS authentication using the provided client certificate and key files. mTLS via
* useClientCert is only supported for Spanner Omni instances.
* useClientCert is only supported for Spanner Omni instances. Certificates and keys are loaded
* dynamically and reloaded automatically when rotated on disk.
*
* @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
*/
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);
}
Preconditions.checkArgument(
!Strings.isNullOrEmpty(clientCertificate), "clientCertificate cannot be null or empty");
Preconditions.checkArgument(
!Strings.isNullOrEmpty(clientCertificateKey),
"clientCertificateKey cannot be null or empty");
this.clientCertificate = clientCertificate;
this.clientCertificateKey = clientCertificateKey;
return this;
}

/**
* Configures the server root CA certificate for SSL/TLS authentication. The CA certificate is
* loaded dynamically and reloaded automatically when rotated on disk.
*
* @param caCertificate Path to the server root CA certificate file.
*/
public Builder setCaCertificate(String caCertificate) {
Preconditions.checkArgument(
!Strings.isNullOrEmpty(caCertificate), "caCertificate cannot be null or empty");
this.caCertificate = caCertificate;
return this;
}
Comment thread
sagnghos marked this conversation as resolved.

Expand Down Expand Up @@ -3175,6 +3231,21 @@ protected boolean shouldRefreshRpc(ServiceRpc cachedRpc) {
return cachedRpc == null || ((SpannerRpc) cachedRpc).isClosed();
}

@Nullable
public String getClientCertificate() {
return clientCertificate;
}

@Nullable
public String getClientCertificateKey() {
return clientCertificateKey;
}

@Nullable
public String getCaCertificate() {
return caCertificate;
}

@SuppressWarnings("unchecked")
@Override
public Builder toBuilder() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import static com.google.cloud.spanner.connection.ConnectionProperties.AUTOCOMMIT;
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.CA_CERTIFICATE;
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;
Expand Down Expand Up @@ -168,6 +169,7 @@ public class ConnectionOptions {
static final String DEFAULT_CREDENTIALS = null;
static final String DEFAULT_CLIENT_CERTIFICATE = null;
static final String DEFAULT_CLIENT_KEY = null;
static final String DEFAULT_CA_CERTIFICATE = 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 @@ -242,6 +244,9 @@ public class ConnectionOptions {
/** Client key path to establish mTLS */
static final String CLIENT_KEY_PROPERTY_NAME = "clientKey";

/** Server root CA certificate path for SSL/TLS */
static final String CA_CERTIFICATE_PROPERTY_NAME = "caCertificate";

/** Name of the 'autocommit' connection property. */
public static final String AUTOCOMMIT_PROPERTY_NAME = "autocommit";

Expand Down Expand Up @@ -676,6 +681,42 @@ public Builder setType(SpannerOptions.InstanceType instanceType) {
return this;
}

/**
* Sets the path to the client certificate file to use for mTLS authentication with Spanner
* Omni.
*
* @param clientCertificate The path to the client certificate file.
* @return this builder
*/
public Builder setClientCertificate(String clientCertificate) {
setConnectionPropertyValue(CLIENT_CERTIFICATE, clientCertificate);
return this;
}

/**
* Sets the path to the client private key file to use for mTLS authentication with Spanner
* Omni.
*
* @param clientCertificateKey The path to the client private key file.
* @return this builder
*/
public Builder setClientCertificateKey(String clientCertificateKey) {
setConnectionPropertyValue(CLIENT_KEY, clientCertificateKey);
return this;
}

/**
* Sets the path to the server root CA certificate file to use for SSL/TLS verification with
* Spanner Omni.
*
* @param caCertificate The path to the root CA certificate file.
* @return this builder
*/
public Builder setCaCertificate(String caCertificate) {
setConnectionPropertyValue(CA_CERTIFICATE, caCertificate);
return this;
}

/**
* @return the {@link ConnectionOptions}
*/
Expand Down Expand Up @@ -1300,6 +1341,10 @@ String getClientCertificateKey() {
return getInitialConnectionPropertyValue(CLIENT_KEY);
}

String getCaCertificate() {
return getInitialConnectionPropertyValue(CA_CERTIFICATE);
}

/**
* 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,7 @@
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.BATCH_DML_UPDATE_COUNT_PROPERTY_NAME;
import static com.google.cloud.spanner.connection.ConnectionOptions.CA_CERTIFICATE_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;
Expand All @@ -42,6 +43,7 @@
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_BATCH_DML_UPDATE_COUNT;
import static com.google.cloud.spanner.connection.ConnectionOptions.DEFAULT_CA_CERTIFICATE;
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;
Expand Down Expand Up @@ -329,6 +331,13 @@ public class ConnectionProperties {
DEFAULT_CLIENT_KEY,
StringValueConverter.INSTANCE,
Context.STARTUP);
static final ConnectionProperty<String> CA_CERTIFICATE =
create(
CA_CERTIFICATE_PROPERTY_NAME,
"Specifies the file path to the server root CA certificate for SSL/TLS validation.",
DEFAULT_CA_CERTIFICATE,
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 @@ -176,6 +176,7 @@ static class SpannerPoolKey {
private final boolean enableEndToEndTracing;
private final String clientCertificate;
private final String clientCertificateKey;
private final String caCertificate;
private final SpannerOptions.InstanceType instanceType;
private final Boolean enableDirectAccess;
private final String universeDomain;
Expand Down Expand Up @@ -221,6 +222,7 @@ private SpannerPoolKey(ConnectionOptions options) throws IOException {
this.enableEndToEndTracing = options.isEndToEndTracingEnabled();
this.clientCertificate = options.getClientCertificate();
this.clientCertificateKey = options.getClientCertificateKey();
this.caCertificate = options.getCaCertificate();
this.instanceType = options.getInstanceType();
this.enableDirectAccess = options.isEnableDirectAccess();
this.universeDomain = options.getUniverseDomain();
Expand Down Expand Up @@ -261,6 +263,7 @@ public boolean equals(Object o) {
&& Objects.equals(this.enableEndToEndTracing, other.enableEndToEndTracing)
&& Objects.equals(this.clientCertificate, other.clientCertificate)
&& Objects.equals(this.clientCertificateKey, other.clientCertificateKey)
&& Objects.equals(this.caCertificate, other.caCertificate)
&& Objects.equals(this.instanceType, other.instanceType)
&& Objects.equals(this.enableDirectAccess, other.enableDirectAccess)
&& Objects.equals(this.universeDomain, other.universeDomain)
Expand Down Expand Up @@ -296,6 +299,7 @@ public int hashCode() {
this.enableEndToEndTracing,
this.clientCertificate,
this.clientCertificateKey,
this.caCertificate,
this.instanceType,
this.enableDirectAccess,
this.universeDomain,
Expand Down Expand Up @@ -540,6 +544,9 @@ Spanner createSpanner(SpannerPoolKey key, ConnectionOptions options) {
if (key.clientCertificate != null && key.clientCertificateKey != null) {
builder.useClientCert(key.clientCertificate, key.clientCertificateKey);
}
Comment thread
sagnghos marked this conversation as resolved.
if (key.caCertificate != null) {
builder.setCaCertificate(key.caCertificate);
}
if (key.instanceType != null) {
builder.setType(key.instanceType);
}
Expand Down
Loading
Loading