diff --git a/bindings/c/test/fdb_c_client_config_tests.py b/bindings/c/test/fdb_c_client_config_tests.py index 60355e44f2..f78a2b1420 100644 --- a/bindings/c/test/fdb_c_client_config_tests.py +++ b/bindings/c/test/fdb_c_client_config_tests.py @@ -14,7 +14,7 @@ from threading import Thread import time from fdb_version import CURRENT_VERSION, PREV_RELEASE_VERSION, PREV2_RELEASE_VERSION from binary_download import FdbBinaryDownloader -from local_cluster import LocalCluster, PortProvider +from local_cluster import LocalCluster, PortProvider, TLSConfig from test_util import random_alphanum_string args = None @@ -36,6 +36,9 @@ class TestCluster(LocalCluster): def __init__( self, version: str, + tls_config: TLSConfig = None, + mkcert_binary: str = None, + disable_server_side_tls: bool = False, ): self.client_config_tester_bin = Path(args.client_config_tester_bin).resolve() assert self.client_config_tester_bin.exists(), "{} does not exist".format( @@ -46,7 +49,16 @@ class TestCluster(LocalCluster): assert self.build_dir.is_dir(), "{} is not a directory".format(args.build_dir) self.tmp_dir = self.build_dir.joinpath("tmp", random_alphanum_string(16)) print("Creating temp dir {}".format(self.tmp_dir), file=sys.stderr) + self.tmp_dir.mkdir(parents=True) + if mkcert_binary: + self.mkcert_binary = Path(mkcert_binary).resolve() + else: + self.mkcert_binary = os.path.join(self.build_dir, "bin", "mkcert") + assert Path(self.mkcert_binary).exists(), "{} does not exist".format( + self.mkcert_binary + ) + self.version = version super().__init__( self.tmp_dir, @@ -54,6 +66,9 @@ class TestCluster(LocalCluster): downloader.binary_path(version, "fdbmonitor"), downloader.binary_path(version, "fdbcli"), 1, + tls_config=tls_config, + mkcert_binary=self.mkcert_binary, + disable_server_side_tls=disable_server_side_tls, ) self.set_env_var("LD_LIBRARY_PATH", downloader.lib_dir(version)) @@ -97,6 +112,10 @@ class ClientConfigTest: self.status_json = None # Configuration parameters to be set directly as needed + self.tls_client_cert_file = None + self.tls_client_key_file = None + self.tls_client_ca_file = None + self.tls_client_disable_plaintext_connection = None self.disable_local_client = False self.disable_client_bypass = False self.ignore_external_client_failures = False @@ -266,6 +285,18 @@ class ClientConfigTest: if self.disable_client_bypass: cmd_args += ["--network-option-disable_client_bypass", ""] + if self.tls_client_cert_file: + cmd_args += ["--network-option-tls_cert_path", self.tls_client_cert_file] + + if self.tls_client_key_file: + cmd_args += ["--network-option-tls_key_path", self.tls_client_key_file] + + if self.tls_client_ca_file: + cmd_args += ["--network-option-tls_ca_path", self.tls_client_ca_file] + + if self.tls_client_disable_plaintext_connection: + cmd_args += ["--network-option-tls_disable_plaintext_connection", ""] + if self.external_lib_path is not None: cmd_args += ["--external-client-library", self.external_lib_path] @@ -337,6 +368,16 @@ class ClientConfigTests(unittest.TestCase): def tearDownClass(cls): cls.cluster.tear_down() + def test_disable_plaintext_connection(self): + # Local client only; Plaintext connections are disabled in a plaintext cluster; Timeout Expected + test = ClientConfigTest(self) + test.print_status = True + test.tls_client_disable_plaintext_connection = True + test.transaction_timeout = 100 + test.expected_error = 1031 # Timeout + test.exec() + test.check_healthy_status(False) + def test_local_client_only(self): # Local client only test = ClientConfigTest(self) @@ -581,6 +622,78 @@ class ClientConfigSeparateCluster(unittest.TestCase): finally: self.cluster.tear_down() + def test_tls_cluster_tls_client(self): + # Test connecting successfully to a TLS-enabled cluster + self.cluster = TestCluster(CURRENT_VERSION, tls_config=TLSConfig()) + self.cluster.setup() + try: + test = ClientConfigTest(self) + test.print_status = True + test.tls_client_cert_file = self.cluster.client_cert_file + test.tls_client_key_file = self.cluster.client_key_file + test.tls_client_ca_file = self.cluster.client_ca_file + test.tls_client_disable_plaintext_connection = True + test.exec() + test.check_healthy_status(True) + finally: + self.cluster.tear_down() + + def test_plaintext_cluster_tls_client(self): + # Test connecting succesfully to a plaintext cluster with a TLS client + self.cluster = TestCluster( + CURRENT_VERSION, tls_config=TLSConfig(), disable_server_side_tls=True + ) + self.cluster.setup() + try: + test = ClientConfigTest(self) + test.print_status = True + test.tls_client_cert_file = self.cluster.client_cert_file + test.tls_client_key_file = self.cluster.client_key_file + test.tls_client_ca_file = self.cluster.client_ca_file + test.exec() + test.check_healthy_status(True) + finally: + self.cluster.tear_down() + + def test_tls_cluster_tls_client_plaintext_disabled(self): + # Test connecting successfully to a TLS-enabled cluster with plain-text connections + # disabled in a TLS-configured client + disable_plaintext_connection = True + tls_config = TLSConfig( + client_disable_plaintext_connection=disable_plaintext_connection + ) + self.cluster = TestCluster(CURRENT_VERSION, tls_config=tls_config) + self.cluster.setup() + try: + test = ClientConfigTest(self) + test.print_status = True + test.tls_client_cert_file = self.cluster.client_cert_file + test.tls_client_key_file = self.cluster.client_key_file + test.tls_client_ca_file = self.cluster.client_ca_file + test.tls_client_disable_plaintext_connection = disable_plaintext_connection + test.exec() + test.check_healthy_status(True) + finally: + self.cluster.tear_down() + + def test_plaintext_cluster_tls_client_plaintext_connection_disabled(self): + # Test connecting succesfully to a plaintext cluster with a TLS-configured client with plaintext connections disabled + self.cluster = TestCluster( + CURRENT_VERSION, tls_config=TLSConfig(), disable_server_side_tls=True + ) + self.cluster.setup() + try: + test = ClientConfigTest(self) + test.tls_client_cert_file = self.cluster.client_cert_file + test.tls_client_key_file = self.cluster.client_key_file + test.tls_client_ca_file = self.cluster.client_ca_file + test.tls_client_disable_plaintext_connection = True + test.transaction_timeout = 100 + test.expected_error = 1031 # Timeout + test.exec() + finally: + self.cluster.tear_down() + # Test client-side tracing class ClientTracingTests(unittest.TestCase): diff --git a/bindings/go/src/fdb/generated.go b/bindings/go/src/fdb/generated.go index 114e8896b3..fd2203c2fd 100644 --- a/bindings/go/src/fdb/generated.go +++ b/bindings/go/src/fdb/generated.go @@ -213,6 +213,11 @@ func (o NetworkOptions) SetTLSPassword(param string) error { return o.setOpt(54, []byte(param)) } +// Prevent client from connecting to a non-TLS endpoint by throwing network connection failed error. +func (o NetworkOptions) SetTLSDisablePlaintextConnection() error { + return o.setOpt(55, nil) +} + // Disables the multi-version client API and instead uses the local client directly. Must be set before setting up the network. func (o NetworkOptions) SetDisableMultiVersionClientApi() error { return o.setOpt(60, nil) diff --git a/documentation/sphinx/source/api-common.rst.inc b/documentation/sphinx/source/api-common.rst.inc index d3e48b7271..4209d0ad4f 100644 --- a/documentation/sphinx/source/api-common.rst.inc +++ b/documentation/sphinx/source/api-common.rst.inc @@ -612,6 +612,10 @@ Sets the passphrase for encrypted private key. Password should be set before setting the key for the password to be used. +.. |option-tls-disable-plaintext-connection| replace:: + + Disable non-TLS connections from the client, allowing only TLS connections. Plaintext connections will timeout. + .. |option-set-disable-local-client| replace:: Prevents connections through the local client, allowing only connections through externally loaded client libraries. diff --git a/documentation/sphinx/source/api-python.rst b/documentation/sphinx/source/api-python.rst index 09bb07327a..b0446ef0e6 100644 --- a/documentation/sphinx/source/api-python.rst +++ b/documentation/sphinx/source/api-python.rst @@ -206,6 +206,10 @@ After importing the ``fdb`` module and selecting an API version, you probably wa |option-tls-password| + .. method :: fdb.options.set_tls_disable_plaintext_connection() + + |option-tls-disable-plaintext-connection| + .. method :: fdb.options.set_disable_local_client() |option-set-disable-local-client| diff --git a/documentation/sphinx/source/tls.rst b/documentation/sphinx/source/tls.rst index dcb0c2c930..53c22fb49d 100644 --- a/documentation/sphinx/source/tls.rst +++ b/documentation/sphinx/source/tls.rst @@ -116,6 +116,8 @@ The value for each setting can be specified in more than one way. The actual va For the password, rather than using the command-line option, it is recommended to use the environment variable ``FDB_TLS_PASSWORD``, as command-line options are more visible to other processes running on the same host. +Clients can disable non-TLS or plaintext connections by setting ``--tls-disable-plaintext-connection``. + As with all other command-line options to ``fdbserver``, the TLS settings can be specified in the :ref:`[fdbserver] section of the configuration file `. The settings for certificate file, key file, peer verification, password and CA file are interpreted by the software. diff --git a/fdbcli/fdbcli.actor.cpp b/fdbcli/fdbcli.actor.cpp index 88d1475017..0c742eece7 100644 --- a/fdbcli/fdbcli.actor.cpp +++ b/fdbcli/fdbcli.actor.cpp @@ -922,6 +922,7 @@ struct CLIOptions { std::string tlsVerifyPeers; std::string tlsCAPath; std::string tlsPassword; + bool tlsDisablePlainTextConnection = false; uint64_t memLimit = 8uLL << 30; std::vector> knobs; @@ -1041,6 +1042,9 @@ struct CLIOptions { case TLSConfig::OPT_TLS_VERIFY_PEERS: tlsVerifyPeers = args.OptionArg(); break; + case TLSConfig::OPT_TLS_DISABLE_PLAINTEXT_CONNECTION: + tlsDisablePlainTextConnection = true; + break; case OPT_HELP: printProgramUsage(program_name.c_str()); @@ -2409,6 +2413,15 @@ int main(int argc, char** argv) { } } + if (opt.tlsDisablePlainTextConnection) { + try { + setNetworkOption(FDBNetworkOptions::TLS_DISABLE_PLAINTEXT_CONNECTION); + } catch (Error& e) { + fprintf(stderr, "ERROR: cannot disable non-TLS connections (%s)\n", e.what()); + return 1; + } + } + try { setNetworkOption(FDBNetworkOptions::DISABLE_CLIENT_STATISTICS_LOGGING); } catch (Error& e) { @@ -2423,6 +2436,7 @@ int main(int argc, char** argv) { printf("\tCertificate Path: %s\n", tlsConfig.getCertificatePathSync().c_str()); printf("\tKey Path: %s\n", tlsConfig.getKeyPathSync().c_str()); printf("\tCA Path: %s\n", tlsConfig.getCAPathSync().c_str()); + printf("\tPlaintext Connection Disable: %s\n", tlsConfig.getDisablePlainTextConnection() ? "true" : "false"); try { LoadedTLSConfig loaded = tlsConfig.loadSync(); printf("\tPassword: %s\n", loaded.getPassword().empty() ? "Not configured" : "Exists, but redacted"); diff --git a/fdbclient/NativeAPI.actor.cpp b/fdbclient/NativeAPI.actor.cpp index 5a9ca300de..740510901c 100644 --- a/fdbclient/NativeAPI.actor.cpp +++ b/fdbclient/NativeAPI.actor.cpp @@ -2543,6 +2543,9 @@ void setNetworkOption(FDBNetworkOptions::Option option, Optional valu tlsConfig.clearVerifyPeers(); tlsConfig.addVerifyPeers(value.get().toString()); break; + case FDBNetworkOptions::TLS_DISABLE_PLAINTEXT_CONNECTION: + tlsConfig.setDisablePlainTextConnection(true); + break; case FDBNetworkOptions::CLIENT_BUGGIFY_ENABLE: enableBuggify(true, BuggifyType::Client); break; diff --git a/fdbclient/vexillographer/fdb.options b/fdbclient/vexillographer/fdb.options index 787b1d27cf..5d362cb814 100644 --- a/fdbclient/vexillographer/fdb.options +++ b/fdbclient/vexillographer/fdb.options @@ -104,6 +104,8 @@ description is not currently required but encouraged.