Skip to content

Commit 1138ca6

Browse files
rmehta19blakeli0
andauthored
feat: Add experimental S2A integration in client libraries grpc transport (#3326)
Modify the Client Libraries gRPC Channel builder to use mTLS via S2A if the experimental environment variable is set, S2A is available (We check this by using utility added in googleapis/google-auth-library-java#1400), and a few more conditions (see `shouldUseS2A`). Following https://google.aip.dev/auth/4115, Only attempt to use S2A after DirectPath and DCA (https://google.aip.dev/auth/4114) are ruled out as options. If conditions to use S2A are not met (env variable not set, or S2A is not running in environment, etc (`shouldUseS2A` returns false)), fall back to default TLS connection. When we are creating S2A-enabled Grpc Channel Credentials, we first try to secure the connection between the client and the S2A via MTLS, using [MTLS-MDS](https://cloud.google.com/compute/docs/metadata/overview#https-mds) credentials. If MTLS-MDS credentials can't be loaded, then we fallback to a plaintext connection between the client and S2A. The parallel go implementation : googleapis/google-api-go-client#1874 (now lives here: https://github.com/googleapis/google-cloud-go/blob/main/auth/internal/transport/cba.go) S2A Java client: https://github.com/grpc/grpc-java/tree/master/s2a Resolving b/376258193 means that S2A.java is no longer experimental --------- Co-authored-by: blakeli <[email protected]>
1 parent b20624c commit 1138ca6

20 files changed

+570
-4
lines changed

WORKSPACE

-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ maven_install(
6565
"com.google.api:gapic-generator-java:" + _gapic_generator_java_version,
6666
] + PROTOBUF_MAVEN_ARTIFACTS + IO_GRPC_GRPC_JAVA_ARTIFACTS,
6767
fail_on_missing_checksum = False,
68-
override_targets = IO_GRPC_GRPC_JAVA_OVERRIDE_TARGETS,
6968
repositories = [
7069
"m2Local",
7170
"https://repo.maven.apache.org/maven2/",

gax-java/dependencies.properties

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ version.io_grpc=1.68.1
3737
# 2) Replace all characters which are neither alphabetic nor digits with the underscore ('_') character
3838
maven.com_google_api_grpc_proto_google_common_protos=com.google.api.grpc:proto-google-common-protos:2.46.0
3939
maven.com_google_api_grpc_grpc_google_common_protos=com.google.api.grpc:grpc-google-common-protos:2.46.0
40-
maven.com_google_auth_google_auth_library_oauth2_http=com.google.auth:google-auth-library-oauth2-http:1.29.0
40+
maven.com_google_auth_google_auth_library_oauth2_http=com.google.auth:google-auth-library-oauth2-http:1.30.0
4141
maven.com_google_auth_google_auth_library_credentials=com.google.auth:google-auth-library-credentials:1.30.0
4242
maven.io_opentelemetry_opentelemetry_api=io.opentelemetry:opentelemetry-api:1.42.1
4343
maven.io_opencensus_opencensus_api=io.opencensus:opencensus-api:0.31.1

gax-java/gax-grpc/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ _COMPILE_DEPS = [
2828
"@io_grpc_grpc_netty_shaded//jar",
2929
"@io_grpc_grpc_grpclb//jar",
3030
"@io_grpc_grpc_java//alts:alts",
31+
"@io_grpc_grpc_java//s2a:s2av2_credentials",
3132
"@io_netty_netty_tcnative_boringssl_static//jar",
3233
"@javax_annotation_javax_annotation_api//jar",
3334
"//gax:gax",

gax-java/gax-grpc/pom.xml

+4
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@
6363
<groupId>io.grpc</groupId>
6464
<artifactId>grpc-protobuf</artifactId>
6565
</dependency>
66+
<dependency>
67+
<groupId>io.grpc</groupId>
68+
<artifactId>grpc-s2a</artifactId>
69+
</dependency>
6670
<dependency>
6771
<groupId>io.grpc</groupId>
6872
<artifactId>grpc-stub</artifactId>

gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/InstantiatingGrpcChannelProvider.java

+155-1
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,24 @@
4646
import com.google.auth.ApiKeyCredentials;
4747
import com.google.auth.Credentials;
4848
import com.google.auth.oauth2.ComputeEngineCredentials;
49+
import com.google.auth.oauth2.SecureSessionAgent;
50+
import com.google.auth.oauth2.SecureSessionAgentConfig;
4951
import com.google.common.annotations.VisibleForTesting;
5052
import com.google.common.base.Preconditions;
53+
import com.google.common.base.Strings;
5154
import com.google.common.collect.ImmutableList;
5255
import com.google.common.collect.ImmutableMap;
5356
import com.google.common.io.Files;
5457
import io.grpc.CallCredentials;
5558
import io.grpc.ChannelCredentials;
5659
import io.grpc.Grpc;
60+
import io.grpc.InsecureChannelCredentials;
5761
import io.grpc.ManagedChannel;
5862
import io.grpc.ManagedChannelBuilder;
5963
import io.grpc.TlsChannelCredentials;
6064
import io.grpc.alts.GoogleDefaultChannelCredentials;
6165
import io.grpc.auth.MoreCallCredentials;
66+
import io.grpc.s2a.S2AChannelCredentials;
6267
import java.io.File;
6368
import java.io.IOException;
6469
import java.nio.charset.StandardCharsets;
@@ -99,6 +104,15 @@ public final class InstantiatingGrpcChannelProvider implements TransportChannelP
99104
@VisibleForTesting
100105
static final String DIRECT_PATH_ENV_ENABLE_XDS = "GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS";
101106

107+
// The public portion of the mTLS MDS root certificate is stored for performing
108+
// cert verification when establishing an mTLS connection with the MDS. See
109+
// https://cloud.google.com/compute/docs/metadata/overview#https-mds-root-certs
110+
private static final String MTLS_MDS_ROOT_PATH = "/run/google-mds-mtls/root.crt";
111+
// The mTLS MDS credentials are formatted as the concatenation of a PEM-encoded certificate chain
112+
// followed by a PEM-encoded private key. See
113+
// https://cloud.google.com/compute/docs/metadata/overview#https-mds-client-certs
114+
private static final String MTLS_MDS_CERT_CHAIN_AND_KEY_PATH = "/run/google-mds-mtls/client.key";
115+
102116
static final long DIRECT_PATH_KEEP_ALIVE_TIME_SECONDS = 3600;
103117
static final long DIRECT_PATH_KEEP_ALIVE_TIMEOUT_SECONDS = 20;
104118
static final String GCE_PRODUCTION_NAME_PRIOR_2016 = "Google";
@@ -107,6 +121,7 @@ public final class InstantiatingGrpcChannelProvider implements TransportChannelP
107121
private final int processorCount;
108122
private final Executor executor;
109123
private final HeaderProvider headerProvider;
124+
private final boolean useS2A;
110125
private final String endpoint;
111126
// TODO: remove. envProvider currently provides DirectPath environment variable, and is only used
112127
// during initial rollout for DirectPath. This provider will be removed once the DirectPath
@@ -126,6 +141,7 @@ public final class InstantiatingGrpcChannelProvider implements TransportChannelP
126141
@Nullable private final Boolean allowNonDefaultServiceAccount;
127142
@VisibleForTesting final ImmutableMap<String, ?> directPathServiceConfig;
128143
@Nullable private final MtlsProvider mtlsProvider;
144+
@Nullable private final SecureSessionAgent s2aConfigProvider;
129145
@VisibleForTesting final Map<String, String> headersWithDuplicatesRemoved = new HashMap<>();
130146

131147
@Nullable
@@ -136,7 +152,9 @@ private InstantiatingGrpcChannelProvider(Builder builder) {
136152
this.executor = builder.executor;
137153
this.headerProvider = builder.headerProvider;
138154
this.endpoint = builder.endpoint;
155+
this.useS2A = builder.useS2A;
139156
this.mtlsProvider = builder.mtlsProvider;
157+
this.s2aConfigProvider = builder.s2aConfigProvider;
140158
this.envProvider = builder.envProvider;
141159
this.interceptorProvider = builder.interceptorProvider;
142160
this.maxInboundMessageSize = builder.maxInboundMessageSize;
@@ -225,6 +243,17 @@ public TransportChannelProvider withEndpoint(String endpoint) {
225243
return toBuilder().setEndpoint(endpoint).build();
226244
}
227245

246+
/**
247+
* Specify whether or not to use S2A.
248+
*
249+
* @param useS2A
250+
* @return A new {@link InstantiatingGrpcChannelProvider} with useS2A set.
251+
*/
252+
@Override
253+
public TransportChannelProvider withUseS2A(boolean useS2A) {
254+
return toBuilder().setUseS2A(useS2A).build();
255+
}
256+
228257
/** @deprecated Please modify pool settings via {@link #toBuilder()} */
229258
@Deprecated
230259
@Override
@@ -410,6 +439,101 @@ ChannelCredentials createMtlsChannelCredentials() throws IOException, GeneralSec
410439
return null;
411440
}
412441

442+
/**
443+
* This method creates {@link TlsChannelCredentials} to be used by the client to establish an mTLS
444+
* connection to S2A. Returns null if any of {@param trustBundle}, {@param privateKey} or {@param
445+
* certChain} are missing.
446+
*
447+
* @param trustBundle the trust bundle to be used to establish the client -> S2A mTLS connection
448+
* @param privateKey the client's private key to be used to establish the client -> S2A mtls
449+
* connection
450+
* @param certChain the client's cert chain to be used to establish the client -> S2A mtls
451+
* connection
452+
* @return {@link ChannelCredentials} to use to create an mtls connection between client and S2A
453+
* @throws IOException on error
454+
*/
455+
@VisibleForTesting
456+
ChannelCredentials createMtlsToS2AChannelCredentials(
457+
File trustBundle, File privateKey, File certChain) throws IOException {
458+
if (trustBundle == null || privateKey == null || certChain == null) {
459+
return null;
460+
}
461+
return TlsChannelCredentials.newBuilder()
462+
.keyManager(privateKey, certChain)
463+
.trustManager(trustBundle)
464+
.build();
465+
}
466+
467+
/**
468+
* This method creates {@link ChannelCredentials} to be used by client to establish a plaintext
469+
* connection to S2A. if {@param plaintextAddress} is not present, returns null.
470+
*
471+
* @param plaintextAddress the address to reach S2A which accepts plaintext connections
472+
* @return {@link ChannelCredentials} to use to create a plaintext connection between client and
473+
* S2A
474+
*/
475+
ChannelCredentials createPlaintextToS2AChannelCredentials(String plaintextAddress) {
476+
if (Strings.isNullOrEmpty(plaintextAddress)) {
477+
return null;
478+
}
479+
return S2AChannelCredentials.newBuilder(plaintextAddress, InsecureChannelCredentials.create())
480+
.build();
481+
}
482+
483+
/**
484+
* This method creates gRPC {@link ChannelCredentials} configured to use S2A to estbalish a mTLS
485+
* connection. First, the address of S2A is discovered by using the {@link S2A} utility to learn
486+
* the {@code mtlsAddress} to reach S2A and the {@code plaintextAddress} to reach S2A. Prefer to
487+
* use the {@code mtlsAddress} address to reach S2A if it is non-empty and the MTLS-MDS
488+
* credentials can successfully be discovered and used to create {@link TlsChannelCredentials}. If
489+
* there is any failure using mTLS-to-S2A, fallback to using a plaintext connection to S2A using
490+
* the {@code plaintextAddress}. If {@code plaintextAddress} is not available, this function
491+
* returns null; in this case S2A will not be used, and a TLS connection to the service will be
492+
* established.
493+
*
494+
* @return {@link ChannelCredentials} configured to use S2A to create mTLS connection to
495+
* mtlsEndpoint.
496+
*/
497+
ChannelCredentials createS2ASecuredChannelCredentials() {
498+
SecureSessionAgentConfig config = s2aConfigProvider.getConfig();
499+
String plaintextAddress = config.getPlaintextAddress();
500+
String mtlsAddress = config.getMtlsAddress();
501+
if (Strings.isNullOrEmpty(mtlsAddress)) {
502+
// Fallback to plaintext connection to S2A.
503+
LOG.log(
504+
Level.INFO,
505+
"Cannot establish an mTLS connection to S2A because autoconfig endpoint did not return a mtls address to reach S2A.");
506+
return createPlaintextToS2AChannelCredentials(plaintextAddress);
507+
}
508+
// Currently, MTLS to MDS is only available on GCE. See:
509+
// https://cloud.google.com/compute/docs/metadata/overview#https-mds
510+
// Try to load MTLS-MDS creds.
511+
File rootFile = new File(MTLS_MDS_ROOT_PATH);
512+
File certKeyFile = new File(MTLS_MDS_CERT_CHAIN_AND_KEY_PATH);
513+
if (rootFile.isFile() && certKeyFile.isFile()) {
514+
// Try to connect to S2A using mTLS.
515+
ChannelCredentials mtlsToS2AChannelCredentials = null;
516+
try {
517+
mtlsToS2AChannelCredentials =
518+
createMtlsToS2AChannelCredentials(rootFile, certKeyFile, certKeyFile);
519+
} catch (IOException ignore) {
520+
// Fallback to plaintext-to-S2A connection on error.
521+
LOG.log(
522+
Level.WARNING,
523+
"Cannot establish an mTLS connection to S2A due to error creating MTLS to MDS TlsChannelCredentials credentials, falling back to plaintext connection to S2A: "
524+
+ ignore.getMessage());
525+
return createPlaintextToS2AChannelCredentials(plaintextAddress);
526+
}
527+
return S2AChannelCredentials.newBuilder(mtlsAddress, mtlsToS2AChannelCredentials).build();
528+
} else {
529+
// Fallback to plaintext-to-S2A connection if MTLS-MDS creds do not exist.
530+
LOG.log(
531+
Level.INFO,
532+
"Cannot establish an mTLS connection to S2A because MTLS to MDS credentials do not exist on filesystem, falling back to plaintext connection to S2A");
533+
return createPlaintextToS2AChannelCredentials(plaintextAddress);
534+
}
535+
}
536+
413537
private ManagedChannel createSingleChannel() throws IOException {
414538
GrpcHeaderInterceptor headerInterceptor =
415539
new GrpcHeaderInterceptor(headersWithDuplicatesRemoved);
@@ -447,16 +571,31 @@ private ManagedChannel createSingleChannel() throws IOException {
447571
builder.keepAliveTime(DIRECT_PATH_KEEP_ALIVE_TIME_SECONDS, TimeUnit.SECONDS);
448572
builder.keepAliveTimeout(DIRECT_PATH_KEEP_ALIVE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
449573
} else {
574+
// Try and create credentials via DCA. See https://google.aip.dev/auth/4114.
450575
ChannelCredentials channelCredentials;
451576
try {
452577
channelCredentials = createMtlsChannelCredentials();
453578
} catch (GeneralSecurityException e) {
454579
throw new IOException(e);
455580
}
456581
if (channelCredentials != null) {
582+
// Create the channel using channel credentials created via DCA.
457583
builder = Grpc.newChannelBuilder(endpoint, channelCredentials);
458584
} else {
459-
builder = ManagedChannelBuilder.forAddress(serviceAddress, port);
585+
// Could not create channel credentials via DCA. In accordance with
586+
// https://google.aip.dev/auth/4115, if credentials not available through
587+
// DCA, try mTLS with credentials held by the S2A (Secure Session Agent).
588+
if (useS2A) {
589+
channelCredentials = createS2ASecuredChannelCredentials();
590+
}
591+
if (channelCredentials != null) {
592+
// Create the channel using S2A-secured channel credentials.
593+
// {@code endpoint} is set to mtlsEndpoint in {@link EndpointContext} when useS2A is true.
594+
builder = Grpc.newChannelBuilder(endpoint, channelCredentials);
595+
} else {
596+
// Use default if we cannot initialize channel credentials via DCA or S2A.
597+
builder = ManagedChannelBuilder.forAddress(serviceAddress, port);
598+
}
460599
}
461600
}
462601
// google-c2p resolver requires service config lookup
@@ -604,7 +743,9 @@ public static final class Builder {
604743
private Executor executor;
605744
private HeaderProvider headerProvider;
606745
private String endpoint;
746+
private boolean useS2A;
607747
private EnvironmentProvider envProvider;
748+
private SecureSessionAgent s2aConfigProvider = SecureSessionAgent.create();
608749
private MtlsProvider mtlsProvider = new MtlsProvider();
609750
@Nullable private GrpcInterceptorProvider interceptorProvider;
610751
@Nullable private Integer maxInboundMessageSize;
@@ -632,6 +773,7 @@ private Builder(InstantiatingGrpcChannelProvider provider) {
632773
this.executor = provider.executor;
633774
this.headerProvider = provider.headerProvider;
634775
this.endpoint = provider.endpoint;
776+
this.useS2A = provider.useS2A;
635777
this.envProvider = provider.envProvider;
636778
this.interceptorProvider = provider.interceptorProvider;
637779
this.maxInboundMessageSize = provider.maxInboundMessageSize;
@@ -648,6 +790,7 @@ private Builder(InstantiatingGrpcChannelProvider provider) {
648790
this.allowNonDefaultServiceAccount = provider.allowNonDefaultServiceAccount;
649791
this.directPathServiceConfig = provider.directPathServiceConfig;
650792
this.mtlsProvider = provider.mtlsProvider;
793+
this.s2aConfigProvider = provider.s2aConfigProvider;
651794
}
652795

653796
/**
@@ -700,12 +843,23 @@ public Builder setEndpoint(String endpoint) {
700843
return this;
701844
}
702845

846+
Builder setUseS2A(boolean useS2A) {
847+
this.useS2A = useS2A;
848+
return this;
849+
}
850+
703851
@VisibleForTesting
704852
Builder setMtlsProvider(MtlsProvider mtlsProvider) {
705853
this.mtlsProvider = mtlsProvider;
706854
return this;
707855
}
708856

857+
@VisibleForTesting
858+
Builder setS2AConfigProvider(SecureSessionAgent s2aConfigProvider) {
859+
this.s2aConfigProvider = s2aConfigProvider;
860+
return this;
861+
}
862+
709863
/**
710864
* Sets the GrpcInterceptorProvider for this TransportChannelProvider.
711865
*

gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/GrpcLongRunningTest.java

+2
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ void setUp() throws IOException {
101101
TransportChannel transportChannel =
102102
GrpcTransportChannel.newBuilder().setManagedChannel(channel).build();
103103
when(operationsChannelProvider.getTransportChannel()).thenReturn(transportChannel);
104+
when(operationsChannelProvider.withUseS2A(Mockito.any(boolean.class)))
105+
.thenReturn(operationsChannelProvider);
104106

105107
clock = new FakeApiClock(0L);
106108
executor = RecordingScheduler.create(clock);

0 commit comments

Comments
 (0)