46
46
import com .google .auth .ApiKeyCredentials ;
47
47
import com .google .auth .Credentials ;
48
48
import com .google .auth .oauth2 .ComputeEngineCredentials ;
49
+ import com .google .auth .oauth2 .SecureSessionAgent ;
50
+ import com .google .auth .oauth2 .SecureSessionAgentConfig ;
49
51
import com .google .common .annotations .VisibleForTesting ;
50
52
import com .google .common .base .Preconditions ;
53
+ import com .google .common .base .Strings ;
51
54
import com .google .common .collect .ImmutableList ;
52
55
import com .google .common .collect .ImmutableMap ;
53
56
import com .google .common .io .Files ;
54
57
import io .grpc .CallCredentials ;
55
58
import io .grpc .ChannelCredentials ;
56
59
import io .grpc .Grpc ;
60
+ import io .grpc .InsecureChannelCredentials ;
57
61
import io .grpc .ManagedChannel ;
58
62
import io .grpc .ManagedChannelBuilder ;
59
63
import io .grpc .TlsChannelCredentials ;
60
64
import io .grpc .alts .GoogleDefaultChannelCredentials ;
61
65
import io .grpc .auth .MoreCallCredentials ;
66
+ import io .grpc .s2a .S2AChannelCredentials ;
62
67
import java .io .File ;
63
68
import java .io .IOException ;
64
69
import java .nio .charset .StandardCharsets ;
@@ -99,6 +104,15 @@ public final class InstantiatingGrpcChannelProvider implements TransportChannelP
99
104
@ VisibleForTesting
100
105
static final String DIRECT_PATH_ENV_ENABLE_XDS = "GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS" ;
101
106
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
+
102
116
static final long DIRECT_PATH_KEEP_ALIVE_TIME_SECONDS = 3600 ;
103
117
static final long DIRECT_PATH_KEEP_ALIVE_TIMEOUT_SECONDS = 20 ;
104
118
static final String GCE_PRODUCTION_NAME_PRIOR_2016 = "Google" ;
@@ -107,6 +121,7 @@ public final class InstantiatingGrpcChannelProvider implements TransportChannelP
107
121
private final int processorCount ;
108
122
private final Executor executor ;
109
123
private final HeaderProvider headerProvider ;
124
+ private final boolean useS2A ;
110
125
private final String endpoint ;
111
126
// TODO: remove. envProvider currently provides DirectPath environment variable, and is only used
112
127
// during initial rollout for DirectPath. This provider will be removed once the DirectPath
@@ -126,6 +141,7 @@ public final class InstantiatingGrpcChannelProvider implements TransportChannelP
126
141
@ Nullable private final Boolean allowNonDefaultServiceAccount ;
127
142
@ VisibleForTesting final ImmutableMap <String , ?> directPathServiceConfig ;
128
143
@ Nullable private final MtlsProvider mtlsProvider ;
144
+ @ Nullable private final SecureSessionAgent s2aConfigProvider ;
129
145
@ VisibleForTesting final Map <String , String > headersWithDuplicatesRemoved = new HashMap <>();
130
146
131
147
@ Nullable
@@ -136,7 +152,9 @@ private InstantiatingGrpcChannelProvider(Builder builder) {
136
152
this .executor = builder .executor ;
137
153
this .headerProvider = builder .headerProvider ;
138
154
this .endpoint = builder .endpoint ;
155
+ this .useS2A = builder .useS2A ;
139
156
this .mtlsProvider = builder .mtlsProvider ;
157
+ this .s2aConfigProvider = builder .s2aConfigProvider ;
140
158
this .envProvider = builder .envProvider ;
141
159
this .interceptorProvider = builder .interceptorProvider ;
142
160
this .maxInboundMessageSize = builder .maxInboundMessageSize ;
@@ -225,6 +243,17 @@ public TransportChannelProvider withEndpoint(String endpoint) {
225
243
return toBuilder ().setEndpoint (endpoint ).build ();
226
244
}
227
245
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
+
228
257
/** @deprecated Please modify pool settings via {@link #toBuilder()} */
229
258
@ Deprecated
230
259
@ Override
@@ -410,6 +439,101 @@ ChannelCredentials createMtlsChannelCredentials() throws IOException, GeneralSec
410
439
return null ;
411
440
}
412
441
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
+
413
537
private ManagedChannel createSingleChannel () throws IOException {
414
538
GrpcHeaderInterceptor headerInterceptor =
415
539
new GrpcHeaderInterceptor (headersWithDuplicatesRemoved );
@@ -447,16 +571,31 @@ private ManagedChannel createSingleChannel() throws IOException {
447
571
builder .keepAliveTime (DIRECT_PATH_KEEP_ALIVE_TIME_SECONDS , TimeUnit .SECONDS );
448
572
builder .keepAliveTimeout (DIRECT_PATH_KEEP_ALIVE_TIMEOUT_SECONDS , TimeUnit .SECONDS );
449
573
} else {
574
+ // Try and create credentials via DCA. See https://google.aip.dev/auth/4114.
450
575
ChannelCredentials channelCredentials ;
451
576
try {
452
577
channelCredentials = createMtlsChannelCredentials ();
453
578
} catch (GeneralSecurityException e ) {
454
579
throw new IOException (e );
455
580
}
456
581
if (channelCredentials != null ) {
582
+ // Create the channel using channel credentials created via DCA.
457
583
builder = Grpc .newChannelBuilder (endpoint , channelCredentials );
458
584
} 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
+ }
460
599
}
461
600
}
462
601
// google-c2p resolver requires service config lookup
@@ -604,7 +743,9 @@ public static final class Builder {
604
743
private Executor executor ;
605
744
private HeaderProvider headerProvider ;
606
745
private String endpoint ;
746
+ private boolean useS2A ;
607
747
private EnvironmentProvider envProvider ;
748
+ private SecureSessionAgent s2aConfigProvider = SecureSessionAgent .create ();
608
749
private MtlsProvider mtlsProvider = new MtlsProvider ();
609
750
@ Nullable private GrpcInterceptorProvider interceptorProvider ;
610
751
@ Nullable private Integer maxInboundMessageSize ;
@@ -632,6 +773,7 @@ private Builder(InstantiatingGrpcChannelProvider provider) {
632
773
this .executor = provider .executor ;
633
774
this .headerProvider = provider .headerProvider ;
634
775
this .endpoint = provider .endpoint ;
776
+ this .useS2A = provider .useS2A ;
635
777
this .envProvider = provider .envProvider ;
636
778
this .interceptorProvider = provider .interceptorProvider ;
637
779
this .maxInboundMessageSize = provider .maxInboundMessageSize ;
@@ -648,6 +790,7 @@ private Builder(InstantiatingGrpcChannelProvider provider) {
648
790
this .allowNonDefaultServiceAccount = provider .allowNonDefaultServiceAccount ;
649
791
this .directPathServiceConfig = provider .directPathServiceConfig ;
650
792
this .mtlsProvider = provider .mtlsProvider ;
793
+ this .s2aConfigProvider = provider .s2aConfigProvider ;
651
794
}
652
795
653
796
/**
@@ -700,12 +843,23 @@ public Builder setEndpoint(String endpoint) {
700
843
return this ;
701
844
}
702
845
846
+ Builder setUseS2A (boolean useS2A ) {
847
+ this .useS2A = useS2A ;
848
+ return this ;
849
+ }
850
+
703
851
@ VisibleForTesting
704
852
Builder setMtlsProvider (MtlsProvider mtlsProvider ) {
705
853
this .mtlsProvider = mtlsProvider ;
706
854
return this ;
707
855
}
708
856
857
+ @ VisibleForTesting
858
+ Builder setS2AConfigProvider (SecureSessionAgent s2aConfigProvider ) {
859
+ this .s2aConfigProvider = s2aConfigProvider ;
860
+ return this ;
861
+ }
862
+
709
863
/**
710
864
* Sets the GrpcInterceptorProvider for this TransportChannelProvider.
711
865
*
0 commit comments