Implement mutual TLS authentication

Add a pair of flags tls_client_certificate / tls_client_key to specify a
certificate and corresponding key, which allow Bazel to authenticate itself
over TLS to a remote cache or remote executor.

Before this change, Bazel only supports Google Cloud authentication, which
requires an open network connection to Google Cloud on the client as well as
on the server.

I have heard from one user that they are tunneling their traffic over a VPN
and then perform no client authentication in their remote caching system. I
heard from another user that they have locally patched Bazel to enable mTLS
(but have not upstreamed the patch). Finally, there is also a pending
feature request.

Compared to other authentication mechanisms, mTLS is already supported by
gRPC out of the box.

I added test coverage by also adding a --tls_ca_certificate to the local
remote worker, and updating the existing TLS test to also run with an mTLS
configuration.

I had to generate a new ca cert/key pair in order to sign a new client
certificate (and also re-generate the server cert/key pair); these are
checked in as testdata. Note that the generator script given there already
supports generating both server and client cert/key pairs, so no change to
the documentation was necessary.

Fixes #10735.

Change-Id: I8c9fdab11d172a4cc8a2b80de43faa48086cc893

Closes #11030.

Change-Id: I8c9fdab11d172a4cc8a2b80de43faa48086cc893
NOKEYCHECK=True
PiperOrigin-RevId: 304041337
diff --git a/src/main/java/com/google/devtools/build/lib/authandtls/AuthAndTLSOptions.java b/src/main/java/com/google/devtools/build/lib/authandtls/AuthAndTLSOptions.java
index c5ab8a2..8dc077c 100644
--- a/src/main/java/com/google/devtools/build/lib/authandtls/AuthAndTLSOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/authandtls/AuthAndTLSOptions.java
@@ -86,15 +86,34 @@
   public boolean incompatibleTlsEnabledRemoved;
 
   @Option(
-    name = "tls_certificate",
-    defaultValue = "null",
-    documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
-    effectTags = {OptionEffectTag.UNKNOWN},
-    help = "Specify the TLS client certificate to use."
-  )
+      name = "tls_certificate",
+      defaultValue = "null",
+      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      help = "Specify a path to a TLS certificate that is trusted to sign server certificates.")
   public String tlsCertificate;
 
   @Option(
+      name = "tls_client_certificate",
+      defaultValue = "null",
+      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      help =
+          "Specify the TLS client certificate to use; you also need to provide a client key to "
+              + "enable client authentication.")
+  public String tlsClientCertificate;
+
+  @Option(
+      name = "tls_client_key",
+      defaultValue = "null",
+      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      help =
+          "Specify the TLS client key to use; you also need to provide a client certificate to "
+              + "enable client authentication.")
+  public String tlsClientKey;
+
+  @Option(
     name = "tls_authority_override",
     defaultValue = "null",
     metadataTags = {OptionMetadataTag.HIDDEN},
diff --git a/src/main/java/com/google/devtools/build/lib/authandtls/GoogleAuthUtils.java b/src/main/java/com/google/devtools/build/lib/authandtls/GoogleAuthUtils.java
index 52c81cd..1fc6cd9 100644
--- a/src/main/java/com/google/devtools/build/lib/authandtls/GoogleAuthUtils.java
+++ b/src/main/java/com/google/devtools/build/lib/authandtls/GoogleAuthUtils.java
@@ -34,6 +34,7 @@
 import io.netty.channel.kqueue.KQueueEventLoopGroup;
 import io.netty.channel.unix.DomainSocketAddress;
 import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -59,11 +60,13 @@
     Preconditions.checkNotNull(target);
     Preconditions.checkNotNull(options);
 
-    final SslContext sslContext =
-        isTlsEnabled(target) ? createSSlContext(options.tlsCertificate) : null;
+    SslContext sslContext =
+        isTlsEnabled(target)
+            ? createSSlContext(
+                options.tlsCertificate, options.tlsClientCertificate, options.tlsClientKey)
+            : null;
 
     String targetUrl = convertTargetScheme(target);
-
     try {
       NettyChannelBuilder builder =
           newNettyChannelBuilder(targetUrl, proxy)
@@ -104,23 +107,40 @@
     return !target.startsWith("grpc://");
   }
 
-  private static SslContext createSSlContext(@Nullable String rootCert) throws IOException {
-    if (rootCert == null) {
+  private static SslContext createSSlContext(
+      @Nullable String rootCert, @Nullable String clientCert, @Nullable String clientKey)
+      throws IOException {
+    SslContextBuilder sslContextBuilder;
+    try {
+      sslContextBuilder = GrpcSslContexts.forClient();
+    } catch (Exception e) {
+      String message = "Failed to init TLS infrastructure: " + e.getMessage();
+      throw new IOException(message, e);
+    }
+    if (rootCert != null) {
       try {
-        return GrpcSslContexts.forClient().build();
-      } catch (Exception e) {
-        String message = "Failed to init TLS infrastructure: " + e.getMessage();
-        throw new IOException(message, e);
-      }
-    } else {
-      try {
-        return GrpcSslContexts.forClient().trustManager(new File(rootCert)).build();
+        sslContextBuilder.trustManager(new File(rootCert));
       } catch (Exception e) {
         String message = "Failed to init TLS infrastructure using '%s' as root certificate: %s";
         message = String.format(message, rootCert, e.getMessage());
         throw new IOException(message, e);
       }
     }
+    if (clientCert != null && clientKey != null) {
+      try {
+        sslContextBuilder.keyManager(new File(clientCert), new File(clientKey));
+      } catch (Exception e) {
+        String message = "Failed to init TLS infrastructure using '%s' as client certificate: %s";
+        message = String.format(message, clientCert, e.getMessage());
+        throw new IOException(message, e);
+      }
+    }
+    try {
+      return sslContextBuilder.build();
+    } catch (Exception e) {
+      String message = "Failed to init TLS infrastructure: " + e.getMessage();
+      throw new IOException(message, e);
+    }
   }
 
   private static NettyChannelBuilder newNettyChannelBuilder(String targetUrl, String proxy)
diff --git a/src/test/shell/bazel/remote/BUILD b/src/test/shell/bazel/remote/BUILD
index d3848dd..0aa83be 100644
--- a/src/test/shell/bazel/remote/BUILD
+++ b/src/test/shell/bazel/remote/BUILD
@@ -47,6 +47,20 @@
     size = "large",
     timeout = "eternal",
     srcs = ["remote_execution_tls_test.sh"],
+    args = ["--tls"],
+    data = [
+        "//src/test/shell/bazel:test-deps",
+        "//src/test/testdata/test_tls_certificate",
+        "//src/tools/remote:worker",
+    ],
+)
+
+sh_test(
+    name = "remote_execution_mtls_test",
+    size = "large",
+    timeout = "eternal",
+    srcs = ["remote_execution_tls_test.sh"],
+    args = ["--mtls"],
     data = [
         "//src/test/shell/bazel:test-deps",
         "//src/test/testdata/test_tls_certificate",
diff --git a/src/test/shell/bazel/remote/remote_execution_tls_test.sh b/src/test/shell/bazel/remote/remote_execution_tls_test.sh
index c9205b9..2436acb 100755
--- a/src/test/shell/bazel/remote/remote_execution_tls_test.sh
+++ b/src/test/shell/bazel/remote/remote_execution_tls_test.sh
@@ -23,12 +23,22 @@
   || { echo "integration_test_setup.sh not found!" >&2; exit 1; }
 
 cert_path="${BAZEL_RUNFILES}/src/test/testdata/test_tls_certificate"
+client_mtls_flags=
+enable_mtls=0
+if [[ $1 == "--mtls" ]]; then
+  enable_mtls=1
+  client_mtls_flags="--tls_client_certificate=${cert_path}/client.crt --tls_client_key=${cert_path}/client.pem"
+fi
 
 function set_up() {
   work_path=$(mktemp -d "${TEST_TMPDIR}/remote.XXXXXXXX")
   cas_path=$(mktemp -d "${TEST_TMPDIR}/remote.XXXXXXXX")
   pid_file=$(mktemp -u "${TEST_TMPDIR}/remote.XXXXXXXX")
   attempts=1
+  mtls_flag=
+  if [[ $enable_mtls == 1 ]]; then
+    mtls_flag=--tls_ca_certificate="${cert_path}/ca.crt"
+  fi
   while [ $attempts -le 3 ]; do
     (( attempts++ ))
     worker_port=$(pick_random_unused_tcp_port) || fail "no port found"
@@ -38,6 +48,7 @@
         --cas_path=${cas_path} \
         --tls_certificate="${cert_path}/server.crt" \
         --tls_private_key="${cert_path}/server.pem" \
+        $mtls_flag \
         --pid_file="${pid_file}" >& $TEST_log &
     local wait_seconds=0
     until [ -s "${pid_file}" ] || [ "$wait_seconds" -eq 15 ]; do
@@ -82,10 +93,25 @@
   bazel build \
       --remote_cache=grpcs://localhost:${worker_port} \
       --tls_certificate="${cert_path}/ca.crt" \
+      ${client_mtls_flags} \
       //a:foo \
       || fail "Failed to build //a:foo with grpcs remote cache"
 }
 
+# Tests that bazel fails if no client cert is provided but the server requires one.
+function test_mtls_fails_if_client_has_no_cert() {
+  # This test only makes sense when we test mtls.
+  [[ $enable_mtls == 1 ]] || return 0
+  _prepareBasicRule
+
+  bazel build \
+      --remote_cache=grpcs://localhost:${worker_port} \
+      --tls_certificate="${cert_path}/ca.crt" \
+      //a:foo 2> $TEST_log \
+      && fail "Expected bazel to fail without a client cert" || true
+  expect_log "ALERT_HANDSHAKE_FAILURE"
+}
+
 function test_remote_grpc_cache() {
   # Test that if default scheme for --remote_cache flag, remote cache works.
   _prepareBasicRule
@@ -93,6 +119,7 @@
   bazel build \
       --remote_cache=localhost:${worker_port} \
       --tls_certificate="${cert_path}/ca.crt" \
+      ${client_mtls_flags} \
       //a:foo \
       || fail "Failed to build //a:foo with grpc remote cache"
 }
@@ -104,6 +131,7 @@
   bazel build \
       --remote_cache=https://localhost:${worker_port} \
       --tls_certificate="${cert_path}/ca.crt" \
+      ${client_mtls_flags} \
       //a:foo \
       || fail "Failed to build //a:foo with https remote cache"
 }
@@ -115,6 +143,7 @@
   bazel build \
       --remote_cache=grpc://localhost:${worker_port} \
       --tls_certificate="${cert_path}/ca.crt" \
+      ${client_mtls_flags} \
       //a:foo \
       && fail "Expected test failure" || true
 }
diff --git a/src/test/testdata/test_tls_certificate/ca.crt b/src/test/testdata/test_tls_certificate/ca.crt
index 0fd16e7..73d3ae5 100644
--- a/src/test/testdata/test_tls_certificate/ca.crt
+++ b/src/test/testdata/test_tls_certificate/ca.crt
@@ -1,27 +1,29 @@
 -----BEGIN CERTIFICATE-----
-MIIEpjCCAo4CCQC/fnOdyO3rEzANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls
-b2NhbGhvc3QwIBcNMTkwNDI1MDY1NjE0WhgPMjk5OTA2MjYwNjU2MTRaMBQxEjAQ
-BgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
-AMe3Q+mfCbfu85lmuUBUd7aQA+kRDtR5K3Rup3cQy6E+k/6eE5D76aTVxVr1HtkY
-1SBs7k2HzbmbNlB5Kfk1IkugIR1F6WAXUpjv6YaWm1TRkSKVFc/pX4pSiNEw6ERu
-Qer4ncakIqBUOG31XGX18n1pFcvwaBT0F8YJe3T8X0/JXDdPEJi6NqOyiok9K/dl
-uTQCeM6Qoh7FC3HXYOnniiE3FnnNT2yciS3JVsFkDdbzh7NUhqLLqXVOf9T0lOVV
-JPfmhQ8EQhJ+PjSPjlwdHMRBHZRyMd6ussie3wXSMAkIcQnV2sC/5D6GEmpwxx7y
-RPFewBbzzFDm+U5x0irqgpLN9i8wRMtQGrKMNZUYB0kItqhTvFEvXn/ls4LjEVjk
-veBL4FTD1R3igzlapZEXVcI4GCghWrxuVMZ6CJFe4VXdFVaHOhra9QDVnJNuyp/q
-a14qBThQjtZMoCxbaM7WAS/Lmr6v6djE72ouA3MZeNc1d4SbigBZUtBuD1Ag4PV3
-aGLlC8pqCWVTOTOPwGdlp9saiXUQQmZ5RTZYdQGBER/fr7CDZ1swSUq1Mr/ZkUi0
-NP2npbh8aHzI8jcCPpC3oD/V89fCDvSbus3NYDvAA2NWTkDs1f8tCDwtsxroPAin
-Tr8Hn9DsTbleGOw3V3NX/1MPHWbasW5gIbtNQ1ydEaePAgMBAAEwDQYJKoZIhvcN
-AQELBQADggIBAIHBTWA3pPyUUVBUcMkojsdGmLAXuOHBOkui9hPNF/7GRkgQY6Ut
-JwqE9ihfZKoCGRyyM45OYGMik6AhscaQvkSy+80UScqasnJwPw7Y2vKwoieo7qw9
-Vp4DAZx1wQND+2M/MlfwROIsZH/usxs1NxIEXCPVzDFbymZSTnGliNraU8yXptxi
-XfOrcAQyjHisI3LVAuinCgsZDSqShPe+ReuWtTv5xP/Pbe8Fkyt3UF4EU8f4dCEh
-9Fpc2YjeZ+5ONx8HqFlo43GAGgoBUGQnVcIC7FVuxF/QaW/So6Tocjo7HyWMV6SK
-FQBL5xpz1AzCLlao2cwYGlIDwsGPLu0/r2WW9lgr0CkL5fnwR3cVb3z6BoezrM1P
-Qc/HMkfbyrvznf9SOarZorQbB9ZNE3WvOK7y9Zx9C5gldzdrdPDkBEWpyYwYzVU2
-XyyRwjjqA0mGCaQoZr1opXNjg/HDt1e3b4p0Od6GRE9mVOTJE5gSh2h5E+VESMtw
-7zuVX6yETnPXJOqeuxZPo5mF98wGXcqAf7/BVuA4kedXEeLRiTH8gC61tckg/O5E
-tLwnxoiNedQVEz0jZT1j3Rh1RkCeMlOYms6giXdOGSci2xmvp9nvqLS0jXURXVQI
-+OlnwnFkeN69ZUOngnnmR3ryTo9kZ6g9ZDxlaq/pp+tKzsvBXRaQOeD1
+MIIFCzCCAvOgAwIBAgIUAvwhUW/jI758b48+1cbMdr62VTIwDQYJKoZIhvcNAQEL
+BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTIwMDMyNjIwMTgxMloYDzMwMDAw
+NTI4MjAxODEyWjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEB
+AQUAA4ICDwAwggIKAoICAQDpcPmUZnp6NAqidLwU/ygtPjbTNU95Lfv7NX1hc2ef
+iXmmj1aIZYAT99LWVZ6h/RdraJrZjrGQr7D+vcDGuZfDMJwE6Vycz9OmDJFlzK94
+r7YzgQpb6DwyjRFOnpTiT9bb7CD6L4aNkxE+EhTDPb2h2VQuIKtxqGBELHE4TaP5
+brWGY1BdISSCin+cTTtEw5Jf2BK0giQddDua4vS5lLbbc3j2KhzHI283tuYWFm1O
+ZoZV7tiR+H7cMaywgVMAeWA7P9kjNtGZlQc4vLuMQSZ11iuuhc2smyIhR1ttjLG0
+2qi3KBaKZA68Q+ALQDScJ/t6lRwv0LghuB1eAH45Hg9VonKL5I1RfN7ZeYle/8Kp
+Y0L66J7U5P+uzHPczIKFG10ncy3UDwDYZA9bIlGD3K7Wn11JzrMtcMmPDJClKkur
++YZ4uhCSZ9txbOJBlNZWVUfWVOB3I6dUnCXp3WEfCU1250hK2vW8FaiSx4o7eSXz
+gHxXjNajcVNTDvyzZ2NGzL8ZW0DRh1naJBTkvfr+5+Qs+r63E51AhfvbHxxZuq+i
+i8eLifqO+//1Bgg7pyazxLObTAsxzH1KKFJuhox0VFABByb4rNMm9fIEM59X62iz
+UbYHibPECdoNMZ4AyFZOmtw14jX4u9EuRqQeI5sP033EdWuzJ/ZISuGnIRcdODSQ
+MQIDAQABo1MwUTAdBgNVHQ4EFgQUwEneLh3hfnfz+VYwLgNfH7Fq4b8wHwYDVR0j
+BBgwFoAUwEneLh3hfnfz+VYwLgNfH7Fq4b8wDwYDVR0TAQH/BAUwAwEB/zANBgkq
+hkiG9w0BAQsFAAOCAgEAC/3Z422v2HGT23dic7Hjx5SZFvqwBv2Se2YGQrd1CfHz
+aY2JdQ6zyeO9v/PBqXndSH2+f5+rnRrx/WenEiLWeGLlNTcEpYUljKsomiHj9QdS
+tk9Heuoq6iR/soRNBnym42SP2ZggxoKK7+yhjTj6e3jR0PwK0crHljqgHEQ0SA0O
+XhsaoiMvhczseAX6QRNrwysCDcX3qC9YQ/S1Ycx2NaBLBs+SKh8k59v/E6xPylZh
+rFciMUhGqYvLZoZ5ma0LdeKvSbG/OLU5ly+25gxD3OUY0rnZl6J/s/QlQsS/YhKe
+oNI5ftyhltrzpOgwQdFxHRSDVqw1c5fe8faJWzbEQo75PytsXLU6/v9xGSvkQi0p
+fnLPEilmhUFunO7V8ouRfGKJ17iPpdQzYokvjQSf8qLPB+Ig8GYdptAUeB31Yt+3
+3uP1SXGBsxTjGvjPUAvK55DINl4PCqnzKWyGpVZaJA40idxLynJQpZTO1MGIuAtY
+Jb1VsLPLNxCkSyzpdEaCIxpbKFpVantzBDtrqA44bW3/57oEclcSMHR0phvQOj0z
+nDHYTqOqdmGNOrcgRhLsNKGjZzCkfSowxuoliAdeRsR0WiAXwEz2JApfTUhDBrG7
+ZR/U/V6PNTWsGsXg4sC9PN+b6TkMlysFHBAzAbHV+lEywRY3t6ssTPRuptiNVKU=
 -----END CERTIFICATE-----
diff --git a/src/test/testdata/test_tls_certificate/client.crt b/src/test/testdata/test_tls_certificate/client.crt
new file mode 100644
index 0000000..4a51444
--- /dev/null
+++ b/src/test/testdata/test_tls_certificate/client.crt
@@ -0,0 +1,27 @@
+-----BEGIN CERTIFICATE-----
+MIIEnjCCAoYCAQEwDQYJKoZIhvcNAQELBQAwFDESMBAGA1UEAwwJbG9jYWxob3N0
+MCAXDTIwMDMyNjIwMTgxNFoYDzMwMDAwNTI4MjAxODE0WjAUMRIwEAYDVQQDDAls
+b2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDF54vGUHKl
+cxyM6S1FHpReStov6FWvhw1FMi54nkU81aqIOu53shmYM0bbqU2jLKO2wDKG+0Yn
+yAMJ/mYeoW3hTSyN6E+Ppa7hzO5pUEE8Bl33c3sECF85YcvHfVdPZjkd6T4bK09h
+c/VJGKUTgR0f79SrFxYx9OuMa2Z0R9/0+P99tbECN18rD9nJGBFEcXPr1BNJvED7
+U6d8TujcRS5crsdEsUH31O7y1X5lhpul3wtoGPjj9voue8QtfhDjji3f7tdIGRgu
+46qc+THxemFKLrXNwJHP82t0sL6ySZ6e/ZodEN2TXdBB2xtEQppAwmLLRx7sCpqf
+FdQ3ZIanlJXjr9JOMhOioxCtq0F6ZYyDnpXJ6qzBZYOjDdy27MEMo7r0SlOTycnh
+GfML6GMH4ojgVicOZsIQgQ/8/tHkQKGD5+I9FuhHTKccIRlHqRGW3EvgFWigxY4/
+nO3/henKlIjn2OywYWR4ROHDkRBN9xwDWGWGMy2ljVqdyTmzGYldNopvJUY2Ux6G
+FZyeoy3NWjIcKVHzNj+UN9kWSpycB37SbWhTm5V1FXoiH5p6eJAcWr2zLlTRlq6e
+5v2Vt47e8DQz9h6d0rniEPQp72RTkyGc5nAgqambGx3MjfCbOUyJZxRlRd+sylPM
+qugkM/Vhg1wq52VsRBr6nbGG2b4urM9jYwIDAQABMA0GCSqGSIb3DQEBCwUAA4IC
+AQAk0TEreEfh0QeZeXHtY1c3w9A5TPbyThTheUVWDLYNXhv9usKACQ4/jlG3Ybat
+2Pxo4xedTQ89xF19CCmB4lthAF2onwYavY+hZOC7IVEfCoA6jZgGpwaw3I0ptug0
+BrQqnBEZFmC8aHqDEgJLgP2tASMQUTl0deNX1rtjvIodaUjPEdyO3QlvPpfq13BM
+y544gYy6ppewjDPnObpDz+FrLoNcA5VPIaimgIF8sBcnd/KDC+WJiE5aEHUFKvD2
+aAt23bTRyrwFtyDmEwvZTh44Kyy+FpeJb8Mud+lEcAeb7FpCQkq/Z4JKf5Lq9SZ9
+of/uKThVW5152l+c8t9E6WveDM30lzu3jqUz8RX3li6yb97xPhrKpMxHzh2iJ9AB
+MD8u7S7HLllPkXuyEOpLg03XlLS/XzRg+FWm7NcpKhe4v17j0/KgoAaKhdkbLJLZ
+vRojvfk+3sQG/R/i92YuqHJ+FzNwqVKJuE1/g737ecuiu8sPSF/ow7w5YaAw3ffo
+FYGHUyZTDKYDj7jcbMAxh9VeKhpvIfIdtNP6mbG1IQ4H18G1q+z7TL8HP5ngwPWP
+HtO4Z4I4X9vRZ+R1466gbHd1zyZBdu3/SIgGPD567fOebv9n1rqcC6w6Ltc60hto
+GVMsFbzqGVU/rGXtHcbE0BII1WLRg0+zgPqw6lVahwoJag==
+-----END CERTIFICATE-----
diff --git a/src/test/testdata/test_tls_certificate/client.pem b/src/test/testdata/test_tls_certificate/client.pem
new file mode 100644
index 0000000..f48d934
--- /dev/null
+++ b/src/test/testdata/test_tls_certificate/client.pem
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+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
+onTQcM9+JyAP+7leDL3gbkWSLPxFJg==
+-----END PRIVATE KEY-----
diff --git a/src/test/testdata/test_tls_certificate/server.crt b/src/test/testdata/test_tls_certificate/server.crt
index 3cfae90..25a9e8a 100644
--- a/src/test/testdata/test_tls_certificate/server.crt
+++ b/src/test/testdata/test_tls_certificate/server.crt
@@ -1,27 +1,27 @@
 -----BEGIN CERTIFICATE-----
-MIIEnjCCAoYCAQEwDQYJKoZIhvcNAQEFBQAwFDESMBAGA1UEAwwJbG9jYWxob3N0
-MCAXDTE5MDQyNTA2NTcxN1oYDzI5OTkwNjI2MDY1NzE3WjAUMRIwEAYDVQQDDAls
-b2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDQJwu4CLzB
-XHkWUaiW7CpHvUyq8SP3tV3l+havZA+tkdhS+U6mo0xQOIZPSzxmj6xBk7elC42N
-0mKKFZQ9ZkErHjmBT0dy/Djlm2dEN8sCN00n74UMWR/QoBygV2hBGgHLGwQ8/9tY
-eVxXh84xikIPnCvlAzbWHd+2gN1RI6n7fz1Z0nO6mk6Q7hi5x6XtBnz2908o0VEb
-JQYSu0LRL2vu7QYmnJ/1bqM0peq5n+ellCt8qiEmcTcw23jHGqlsyTy9vrJCO0Up
-LKZ2SR9wewbAnImZAPDWytQEVpCP6eEiIdNyDvYsTeoUPECklNUi9Iy25gZPeptC
-x8YWeZGZXen/Nct416bH8VrYqmjhArXfimUiw/TdREOlvVFmcBpuvTKXKVpvuyIf
-XK9mQVg7ITRnXIvJBoapcV0EaaFykCvRMDvGqwiR05da+ZlH6TykJ74BV0y1NFY0
-XJrzhYvGPYF3nP3wDqHd4BJBNecT+26GN5Y3Z0fyBCf52nyhWKU+Cj1bXILNs9wt
-A08HL1B09vFpm8TIBYnUDbQQpYumwiN0qzlRuXOR5AfmvFJoVSFvD0BW4itX0nY+
-3ep5Zia4TRbhYOSHPPaln4RfxEWh8ERh9EU3TAPbqv/Rl0HUL4e3kN32pamRH9+8
-7j2bet8BoDngwgyHFYAtD9j/LtmEhRKSzQIDAQABMA0GCSqGSIb3DQEBBQUAA4IC
-AQBogMIcx+S+X/hZFCLcZIfGCqDjkscnrEM0pMd3xZJq34ETlwB0gnNC6jVjFgmI
-IDcY3ShezLNeMB61p3DPtDm7ILJZgtmtXFzasxuTcvvPg6YPKoz4h1hAmt7BIDDW
-nAxUVdm8mYaU1GVzxgqeWsn38tZtr02IwZxpNn4J/tirs/lCB++NnyaIY0HZUVp9
-3XVa1hPv2R4E4lr1hyqHECU5rllM/qMV8sWdg5TVw/TJa3RFmNL7wXUEGxtrlwb1
-kSVjzq+CvUSIzj6MG9EbPhghEQ/GdbmRileUBA4/LXb48eyP8KMgYhxMEX4HQGoX
-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
+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
 -----END CERTIFICATE-----
diff --git a/src/test/testdata/test_tls_certificate/server.pem b/src/test/testdata/test_tls_certificate/server.pem
index 8c5606a..0dec6ed 100644
--- a/src/test/testdata/test_tls_certificate/server.pem
+++ b/src/test/testdata/test_tls_certificate/server.pem
@@ -1,52 +1,52 @@
 -----BEGIN PRIVATE KEY-----
-MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDQJwu4CLzBXHkW
-UaiW7CpHvUyq8SP3tV3l+havZA+tkdhS+U6mo0xQOIZPSzxmj6xBk7elC42N0mKK
-FZQ9ZkErHjmBT0dy/Djlm2dEN8sCN00n74UMWR/QoBygV2hBGgHLGwQ8/9tYeVxX
-h84xikIPnCvlAzbWHd+2gN1RI6n7fz1Z0nO6mk6Q7hi5x6XtBnz2908o0VEbJQYS
-u0LRL2vu7QYmnJ/1bqM0peq5n+ellCt8qiEmcTcw23jHGqlsyTy9vrJCO0UpLKZ2
-SR9wewbAnImZAPDWytQEVpCP6eEiIdNyDvYsTeoUPECklNUi9Iy25gZPeptCx8YW
-eZGZXen/Nct416bH8VrYqmjhArXfimUiw/TdREOlvVFmcBpuvTKXKVpvuyIfXK9m
-QVg7ITRnXIvJBoapcV0EaaFykCvRMDvGqwiR05da+ZlH6TykJ74BV0y1NFY0XJrz
-hYvGPYF3nP3wDqHd4BJBNecT+26GN5Y3Z0fyBCf52nyhWKU+Cj1bXILNs9wtA08H
-L1B09vFpm8TIBYnUDbQQpYumwiN0qzlRuXOR5AfmvFJoVSFvD0BW4itX0nY+3ep5
-Zia4TRbhYOSHPPaln4RfxEWh8ERh9EU3TAPbqv/Rl0HUL4e3kN32pamRH9+87j2b
-et8BoDngwgyHFYAtD9j/LtmEhRKSzQIDAQABAoICAFKCR7jpbbjP6QeZ0tQQRSou
-tUdFUtaLw+63VWqspTJOD4vEWxLexA9AeKzRy91zsfpEjZUUoUXIUVse9qXn9Ikc
-7/p77Hx90ifhk+uMmiIEvcbIwNqGMYBHF1HPk/nKT0+tI97yJIZLhPkFUgx9G3aI
-lzWuMnxpVxZGunPBSU3xv+Xs8AbVx7LXTmHF69WqrrpOichKQHYcFO4z4GZ+/6+z
-sK55g0aMVpE1+3cdFXui4iIGZiGQ2ym4tYVm4iXHFxa3kn2CdQW/NzTIA3hYq/KJ
-mllV8CGUQLp/fcouERmwgtpBZ/9j2xeuUolqnZm/ik+tmm7C0OyFt9WM1tQOUAQs
-0GNJTq/bpnx3igK5qCDrI9nRdVzGHsdZnefKc1QIr/MCEo1kHtFJeHPHGNDIWENW
-UpWfI63dA2yKSBAYSz5ictuELF9Xa2uCX9J/D4nZl/J0SoqvFZ2GxADmGajVugBF
-RlNbgqSjTSFG+5//GvwBESchaAarwtPaLAkK3M/s8z0BO2vOz7RmyicaarwgbBvn
-OBhSOuaIUXAq1qNd3zqpirt+pP6+xo2A+erTqgJ0dGiFDyzhT7anzNA5SVvlBYcg
-89aWHdgwcwXCM3ILzmdq8bPIUAp9wi041Bp15rhnKR7m5ZCg69I1p1PLjp87lM71
-omtJ0lWcy+uAU15akdMZAoIBAQDpUhgkk/v7otTAiY0+G8IKVtudOf0xo8Hz/WWs
-7EpZIxBgv3x15fsHsg09ORbImd8Q4kvgodvcTzOc5pB7Mr2GXty8i0YfEOl6XzKB
-YCAyoI57iNycFu/hfiOuo6pHIe8j2kA3ik5DOxLk3DMyQgCUwpmKZmQT0c62NUMZ
-HTvAwfdYqwNmBRLdlVDGEg9atOa6grqC2HjPUmL+hMKlpx+Fx6sb6oWYZDu+4Efg
-cdzfyhxYIXFydJ+JttWd8bS/A3C01Y13WWJ5Zwe9awh2YrDYX1uT6IB/stwAnpN/
-W/JB0TCkZlkfsRp7nvSLYu3sOjs+8KCcOPu8Ylof5CXrDgJ3AoIBAQDkYqwHvlUN
-Xb86Pavo6tFfIloZMq8U0RGlNXrG+Rm6Z+eTAEyVvnu4ApkD/19N0AGKGdSjkKzb
-xt4gOyU/IxHplu1zCJButVx9PiysXpX12KTlsEP0B2f+JHtirWMPCmPrkFsh8l5h
-Uls7c8eDABE7+UdQrf63OZMYxRRt3liiasvRTg256M79/JYQJBmKu4NC0UhRpRH5
-LwByeY5QIxXGN1JdmDXTFh2A/UEjLlF+9ZklyDXgU2Aljkf6BAuNlsJ1rABvG3nC
-dD0cqO4xVBIacrYTKgL+PULlmTpSdnwILlthEDdxrojj7mZrVPJklzkWsiwVYIEs
-+fZ5TLF/jAHbAoIBAQCZZjAZXHI/bz8Rl14Vh4p74b9iD8435MKP9/nxRylakYMj
-GMJrgVkaJiYuKmqgWQofv6jDd6dloWz9q1kyppmUzqmyDJ99rVDT8+LwzJJettD2
-x3TD6xCr4JL1LwW03sqrd8LgwT3TVfOGJIBEesCHDaqFI+yIW1jc0wfaay3t/Zjx
-4v3JBWzx4knI7/bIXEeWOH0HqetD45bSX9bZspc3DZ+iKv7KwpvFUw/usO3W9LrN
-9q7v4V1C3cJ0pYWAUHK5ce4gmdP0nZipIMXfj+NVXtyG0kYprx6WCaxP/9O3EiI4
-9FGEVJxkyo1dVx22QlLRfsMZ8x0PLlqyvP1xHTThAoIBAQCdn1gZlAwBSJVFLfEq
-tH3CCeRjBa7+T/i8q/dLwfo2w6V4uDkjFC8w5WIT9zkgbBHT7VXreVtD57HATvG6
-7IpdTBQfU2bTcYoeyj1szW70GQxdldSgZEgqh6U8imwWolYp6xxqhmsLAhsDIjot
-OGusl7PXg+6LKEpUSxh5Z36GwexfTV5906agdqZfB3s1W4sRH32pE6Me9oh5eVl2
-B3Dst5u6CuYDBH1iW+eLz1jhpcGH6PD+HKz73oHglNAgbU9ShV5bUHwtb6oJ0LFs
-DBjedhMhkNo1+7Pi4Gj3Jt0djFj22YlahVnm7c9z/lG4iQIWnut76Xndv7qTJxJN
-9CQHAoIBAD0Lf4U/LI2BeWmGUeCZs70EWg5eKjcfDPq4yscl+ga3q1STO3A7kzJp
-rTfNL/pZyEJR0YBL+XEWnTfM9Jk28JE2wUGFLjIpSR5/lT502TPwpanO6/QAsvqz
-6Gy5ieygM6iDewVTaB6550JwGndRj1KlDRIOgq9xwCK6zMM7EpvrMRBF9mlO4kh5
-FUd+gpdseE98voURwlFC6A5/D03d3oTBt5wG8CZjplhX0Mnt9S3o77Kx9Odm/jbe
-90GuUbJnbRVb9g21bLlc+jcxNTc1VEQmMo/qdQL7HoTE5Igz4yBz3RGpNk5GLbSy
-QZdE11Pf2YMgwLAKdkZ1N6pOwQRpLFA=
+MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDEtHPdOrBRZLtf
+RCSeYdgydj/DBZRfCoHm2uZuBblo31pP17sGoo57d/yZNw+naEZIfGVNZDWEteBX
+QyR/zi+8cHelDiV+wLxprc9sCFYgssK/XPY2+iZBx1c00Vz7isK7dGcp/VRnZryp
+YhCnSoOmg5UmxGCHOdp5xHZdGXJznDLd5pL/PRKlszX8nLa4kKGbbMwYH/bF2jHo
+MGN9ViS+C/cAuRaF5Cj2p9OvywC/9cy1GiincYRE7auMCbBZgF0JueCCCJ7EBy2c
+oNVXT/Sx2X+pRwM13kV1kcrcIyt8Kt3qk/kfF0wkLkPoHmbkhDb5b4jFiSqfBHjz
+uZfqsFSQ0QLYp0tTMDPe6Jm8PbJXVxEseGfkX5MK73Pxe5pid38xWHA907K/K9yC
+PQGbnUNMNaYlAPb2aMgS0N86Pr9RJUhxX5RaksCpHiod5YA1JQQtsUf36zkqUSWF
+4eApCuu2myK8zm+xtWBuI3Njllmcd91ZeMWkVb46alQfbI+n8wy6WcV/NC+Eon9D
+UtUfwaetBQ2nWp7MmN1hpA2oypYa8TYDx6ZlbL8vpkW4KpBvlZdYxeE34ltFBwsr
+oHL58mLBA712xZMa54GYNe3rKd4waQFhD41RAe1H9Y5PhDJpjEmAXEyTX9FLRcHR
+pAkTdcBDQLy4bLAGXmyK6/L1Bw3kWQIDAQABAoICAQC9HsovL5gKCZFk3L1gUa5t
+hedz989ZOV7/uALIUVScEfJgxYeZr3zSFOCV5qx0RfsdAgzbxbb2627QN0vGXVTk
+FjXSSbGfFmuQJ34/3hwAwB4hop1O6l8R6zhbHdgKOLVVSWtOobQe3lYRfKmKTkgZ
+NnWWmkQ8f1EgtdUfWbICmXEGjANUx0FAcvc68ulytgvKxWXM5B58x3YoSS2+ea5F
+0ncfCNUw0dbYny8V21XTOd4hWQ8xPiDvrJq8vywAQTwyd7X1D5il3EjsSG4Vzlfz
+DqyA8jeR+SxLB2tFD8NlVEmcmbxxOhMIzjqX13MRzlSUqbmUQnbqAIDRw+Tdzb7e
+9UZPLhg0yLM4JY5Vd11rzed3hiajeOyxvr1jHP1JoNNvdT0ZlNIrInZLKqunZ9I8
+5ntr6o4uJyAL8DODR26+HPi3NzJJFniGDhCAyPzuKc6fRtNXgOc/6/IdCPQiwYF5
+KjIWlBWGG7zNMUE0fUJx1+okpmKZ6aqIoM/qfR1jY52HsHeOLYR30FVCy+lo+vVD
+ysNJhVA1vNNGmV2gwIJemBg7b+cjVqB+ATc8NEzfgCSiSEp3Q9+j0/wgKeD8Lq5+
+DOycXaEhJRmo7SogzTVdQN54D0ujJ4QNgo98swV3+H+14tSeFTDMPg5neZ+dCdoU
+NVKoKtV6y/SyFngana/EIQKCAQEA6p/zc1gLnLC6Ej1Ql5DWqy/M8Xa3E/IcqgpR
++8WxiPCSerf1Kirq9UgF4YQoTZzkQIcHiQahdnufLVwNKqXEp0HC8MV7G3+3LnOo
+Drbe1J7XqSyxSx0ruDuN587X+OqecGmK5X9mpFH+2McuC9CeaT/uKsxfOov7gNE0
+RvQoZTWhUeSkYrEUwtmr2BF2IsTU6tRLawRiqrnD/FMuY0C+YiJimk5q/klaGiPC
+MGuQvmkHsrhxJcSj4aCKJqsIv7sEEQkB8e9VU+jnhWlUpl5VUwCzuLorp12TveRX
+/moiWT+KzK3KwQozHj8Y7pdOADtkKacDZgk1IenoTuQ6Q8VRZQKCAQEA1qAcyga4
+hx7ekC1s6vOvacO5jaD+fkJYTj2ZP8hQLcM3j7gTzg589w5ANQL0jWxJxpeuu0zn
+rMpnXi+roeKMPdZ+yLy4DZrgzAGUMtwgkVVMtreSgI3hgrbi9Z495Sha42hAdEcT
+KjOjdVr4MLW8cuLTmIFwakP9tthxvq4DwUnTyJtL0319MDmYvg1mTk+L8LByueCP
+Av5gzJwrN9vL88AsKJA8Souf7Vfq9ef6MhUgFM5GLb4+Uq9ZYnB6GdYsZweX3R3I
+uGLl4pWL4TydInwebvvUVf1jeEy+acOmi+DfyC5gkk0bCRXdDgz6PujqLRHEq4X9
+s9R+R18ORXzx5QKCAQEAibeBar7PchW54mLjH1QA7VKNdV49cBO5B4YvQR11a+/p
+yuaXnTy71WWFLi4oigYBZG7d2Wxu8eD2OeXCRLowiAxtpG4GKMn6d+WjS5/DhAII
+jGCTYIeq1eT/EoWy94Sfo1QQF02ErgcDE7M2L/EwSo8f+Tck3nS0F5S0nsFJxL6K
+Bkuywcs3aHfkCluVgCsQ3xXlfteAIr4Pb9hTbibemTOdtP06iC/+F0HOBiXdPCbi
+QeFJaOXXW+SjsrbJ1+CqLmWfIqdc6nfXDdQZv923L5VF6LQ+U2r2AYw6qjcaGlDV
+4/ZPAKhAAQ0AUWu2eSRjUp+ZuxbEfTeTCFumZ4k2kQKCAQBtVpY0CaZ6F6zUkH+7
+VjeXzwE5eLoNwmjQOytWRgsqtRgaHHHieJkLF3R4TTAe1/rhtCZs/unLqjVs0yZB
+y3Mckah3RUUSkUNSSr+gBWqF/4mcT/rPiPhIqjkHXf00QBHFZjfnxMmrpzDvuU9V
+KVB+yrV3LQIC8O5Q9wVDWc1J6/17ZjoD3RsotT7uG09yN64YCRv5O8A/iy3vLuQJ
+iezmGZGlfI1qgKURucdWTT61wvNcBhXUeeWwI+qKbriVbvmh50ljeSfnX2KzwvHG
+5iU7CzZJ3fs3b2X8RESBBw5SllYK2i2Sert6Lmw2G0BlSiz6luG1bAZqVaebXn6b
+weJNAoIBAGN2Vmr+F/veEsrMMtdbSV/hVcdBSf8TT2RWU+tlcRy0bF87xC3jztnF
+aHVuUhvPzxVX/6/lmrjx3TYT88rZN/GITwsOjKmjhXpmupi0K0qPobyMAsm+9fHt
+vQ3pWHuxvM+IjCYlCT+nxvy0VNJ0j4b8UTG8huKf8/yj0WI+eDwX7b9+5ZDcQb2/
+BJVAnb3b8e8orENkIhwr1juSOo9pRjN+QghbP2enGEJs4BZhm/rjBiXvQ6PuvumW
+ovb2OUxUrEc3qZuj6LWgAvLuOhG5VW1onbbvN1UHzWl43Ql1k3gks12U97eruAfp
+kFXtZAPNBO0IKMh9aPrUgaiakUeV1pQ=
 -----END PRIVATE KEY-----
diff --git a/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/RemoteWorker.java b/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/RemoteWorker.java
index e4bd8de..0bd3791 100644
--- a/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/RemoteWorker.java
+++ b/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/RemoteWorker.java
@@ -63,6 +63,7 @@
 import io.netty.channel.socket.nio.NioServerSocketChannel;
 import io.netty.handler.logging.LogLevel;
 import io.netty.handler.logging.LoggingHandler;
+import io.netty.handler.ssl.ClientAuth;
 import io.netty.handler.ssl.SslContextBuilder;
 import io.netty.handler.ssl.SslProvider;
 import java.io.File;
@@ -176,10 +177,14 @@
   }
 
   private SslContextBuilder getSslContextBuilder(RemoteWorkerOptions workerOptions) {
-    SslContextBuilder sslClientContextBuilder =
+    SslContextBuilder sslContextBuilder =
         SslContextBuilder.forServer(
             new File(workerOptions.tlsCertificate), new File(workerOptions.tlsPrivateKey));
-    return GrpcSslContexts.configure(sslClientContextBuilder, SslProvider.OPENSSL);
+    if (workerOptions.tlsCaCertificate != null) {
+      sslContextBuilder.clientAuth(ClientAuth.REQUIRE);
+      sslContextBuilder.trustManager(new File(workerOptions.tlsCaCertificate));
+    }
+    return GrpcSslContexts.configure(sslContextBuilder, SslProvider.OPENSSL);
   }
 
   private void createPidFile() throws IOException {
diff --git a/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/RemoteWorkerOptions.java b/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/RemoteWorkerOptions.java
index c6b876a..78a040f 100644
--- a/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/RemoteWorkerOptions.java
+++ b/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/RemoteWorkerOptions.java
@@ -166,6 +166,16 @@
       help = "Specify the TLS private key to be used.")
   public String tlsPrivateKey;
 
+  @Option(
+      name = "tls_ca_certificate",
+      defaultValue = "null",
+      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      help =
+          "Specify a CA certificate to use for authenticating clients; setting this implicitly "
+              + "requires client authentication (aka mTLS).")
+  public String tlsCaCertificate;
+
   private static final int MAX_JOBS = 16384;
 
   /**