aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTor Brede Vekterli <vekterli@yahooinc.com>2022-08-22 13:55:17 +0000
committerTor Brede Vekterli <vekterli@yahooinc.com>2022-08-22 14:11:59 +0000
commit55e0f93a74214caf22c4fd79d60e1b0b6836a99c (patch)
tree0a59f93c8c46f98007057ab9e49a789867e2018d
parentb4117991e6d98dbc9a93625f6dc7170cdd1484dc (diff)
Support capability enforcement environment variable in C++
Mirrors Java enforce/log-only/disable semantics, defaulting to enforce. Also fixes an issue where connection auth context and capabilities would not be set if a server socket was running in mixed-mode. This is not a problem in practice since mixed-mode is inherently completely insecure since it must accept plain-text clients, which implicitly have all capabilities granted.
-rw-r--r--fnet/src/tests/frt/rpc/CMakeLists.txt6
-rw-r--r--fnet/src/tests/frt/rpc/invoke.cpp34
-rw-r--r--fnet/src/tests/frt/rpc/my_crypto_engine.hpp9
-rw-r--r--fnet/src/vespa/fnet/frt/require_capabilities.cpp14
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt1
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/capability_env_config.cpp46
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/capability_env_config.h16
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/maybe_tls_crypto_socket.cpp5
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/maybe_tls_crypto_socket.h1
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/transport_security_options.cpp13
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/transport_security_options.h4
-rw-r--r--vespalib/src/vespa/vespalib/test/make_tls_options_for_testing.cpp23
-rw-r--r--vespalib/src/vespa/vespalib/test/make_tls_options_for_testing.h8
13 files changed, 166 insertions, 14 deletions
diff --git a/fnet/src/tests/frt/rpc/CMakeLists.txt b/fnet/src/tests/frt/rpc/CMakeLists.txt
index 64a1d150fce..35150cad7b6 100644
--- a/fnet/src/tests/frt/rpc/CMakeLists.txt
+++ b/fnet/src/tests/frt/rpc/CMakeLists.txt
@@ -10,6 +10,12 @@ vespa_add_test(NAME fnet_invoke_test_app_xor COMMAND fnet_invoke_test_app ENVIRO
vespa_add_test(NAME fnet_invoke_test_app_tls COMMAND fnet_invoke_test_app ENVIRONMENT "CRYPTOENGINE=tls")
vespa_add_test(NAME fnet_invoke_test_app_tls_maybe_yes COMMAND fnet_invoke_test_app ENVIRONMENT "CRYPTOENGINE=tls_maybe_yes")
vespa_add_test(NAME fnet_invoke_test_app_tls_maybe_no COMMAND fnet_invoke_test_app ENVIRONMENT "CRYPTOENGINE=tls_maybe_no")
+vespa_add_test(NAME fnet_invoke_test_app_tls_cap_enforced COMMAND fnet_invoke_test_app
+ ENVIRONMENT "CRYPTOENGINE=tls" "VESPA_TLS_CAPABILITIES_ENFORCEMENT_MODE=enforce")
+vespa_add_test(NAME fnet_invoke_test_app_tls_cap_log_only COMMAND fnet_invoke_test_app
+ ENVIRONMENT "CRYPTOENGINE=tls" "VESPA_TLS_CAPABILITIES_ENFORCEMENT_MODE=log_only")
+vespa_add_test(NAME fnet_invoke_test_app_tls_cap_disable COMMAND fnet_invoke_test_app
+ ENVIRONMENT "CRYPTOENGINE=tls" "VESPA_TLS_CAPABILITIES_ENFORCEMENT_MODE=disable")
vespa_add_executable(fnet_detach_return_invoke_test_app TEST
SOURCES
detach_return_invoke.cpp
diff --git a/fnet/src/tests/frt/rpc/invoke.cpp b/fnet/src/tests/frt/rpc/invoke.cpp
index e1912985379..2668d86cae6 100644
--- a/fnet/src/tests/frt/rpc/invoke.cpp
+++ b/fnet/src/tests/frt/rpc/invoke.cpp
@@ -1,6 +1,7 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
#include <vespa/vespalib/testkit/test_kit.h>
#include <vespa/vespalib/net/socket_spec.h>
+#include <vespa/vespalib/net/tls/capability_env_config.h>
#include <vespa/vespalib/util/benchmark_timer.h>
#include <vespa/vespalib/util/latch.h>
#include <vespa/fnet/frt/supervisor.h>
@@ -8,12 +9,14 @@
#include <vespa/fnet/frt/rpcrequest.h>
#include <vespa/fnet/frt/invoker.h>
#include <vespa/fnet/frt/request_access_filter.h>
+#include <vespa/fnet/frt/require_capabilities.h>
#include <mutex>
#include <condition_variable>
#include <string_view>
using vespalib::SocketSpec;
using vespalib::BenchmarkTimer;
+using namespace vespalib::net::tls;
constexpr double timeout = 60.0;
constexpr double short_timeout = 0.1;
@@ -221,6 +224,13 @@ public:
rb.DefineMethod("accessRestricted", "s", "",
FRT_METHOD(TestRPC::RPC_AccessRestricted), this);
rb.RequestAccessFilter(std::make_unique<MyAccessFilter>());
+ // The authz rules used for this test only grant the telemetry capability set
+ rb.DefineMethod("capabilityRestricted", "", "",
+ FRT_METHOD(TestRPC::RPC_AccessRestricted), this);
+ rb.RequestAccessFilter(std::make_unique<FRT_RequireCapabilities>(CapabilitySet::content_node()));
+ rb.DefineMethod("capabilityAllowed", "", "",
+ FRT_METHOD(TestRPC::RPC_AccessRestricted), this);
+ rb.RequestAccessFilter(std::make_unique<FRT_RequireCapabilities>(CapabilitySet::telemetry()));
}
void RPC_Test(FRT_RPCRequest *req)
@@ -470,6 +480,30 @@ TEST_F("request allowed by access filter invokes server method as usual", Fixtur
EXPECT_TRUE(f1.server_instance().restricted_method_was_invoked());
}
+TEST_F("capability checking filter is enforced under mTLS unless overridden by env var", Fixture()) {
+ MyReq req("capabilityRestricted"); // Requires content node cap set; disallowed
+ f1.target().InvokeSync(req.borrow(), timeout);
+ auto cap_mode = capability_enforcement_mode_from_env();
+ fprintf(stderr, "capability enforcement mode: %s\n", to_string(cap_mode));
+ if (crypto->use_tls_when_client() && (cap_mode == CapabilityEnforcementMode::Enforce)) {
+ // Default authz rule does not give required capabilities; must fail.
+ EXPECT_EQUAL(req.get().GetErrorCode(), FRTE_RPC_PERMISSION_DENIED);
+ EXPECT_FALSE(f1.server_instance().restricted_method_was_invoked());
+ } else {
+ // Either no mTLS configured (implicit full capability set) or capabilities not enforced.
+ ASSERT_FALSE(req.get().IsError());
+ EXPECT_TRUE(f1.server_instance().restricted_method_was_invoked());
+ }
+}
+
+TEST_F("access is allowed by capability filter when peer is granted the required capability", Fixture()) {
+ MyReq req("capabilityAllowed"); // Requires telemetry cap set; allowed
+ f1.target().InvokeSync(req.borrow(), timeout);
+ // Should always be allowed, regardless of mTLS mode or capability enforcement
+ ASSERT_FALSE(req.get().IsError());
+ EXPECT_TRUE(f1.server_instance().restricted_method_was_invoked());
+}
+
TEST_MAIN() {
crypto = my_crypto_engine();
TEST_RUN_ALL();
diff --git a/fnet/src/tests/frt/rpc/my_crypto_engine.hpp b/fnet/src/tests/frt/rpc/my_crypto_engine.hpp
index 83934c430b3..219b4dafd05 100644
--- a/fnet/src/tests/frt/rpc/my_crypto_engine.hpp
+++ b/fnet/src/tests/frt/rpc/my_crypto_engine.hpp
@@ -17,14 +17,17 @@ vespalib::CryptoEngine::SP my_crypto_engine() {
return std::make_shared<vespalib::XorCryptoEngine>();
} else if (engine == "tls") {
fprintf(stderr, "crypto engine: tls\n");
- return std::make_shared<vespalib::TlsCryptoEngine>(vespalib::test::make_tls_options_for_testing());
+ return std::make_shared<vespalib::TlsCryptoEngine>(
+ vespalib::test::make_telemetry_only_capability_tls_options_for_testing());
} else if (engine == "tls_maybe_yes") {
fprintf(stderr, "crypto engine: tls client, mixed server\n");
- auto tls = std::make_shared<vespalib::TlsCryptoEngine>(vespalib::test::make_tls_options_for_testing());
+ auto tls = std::make_shared<vespalib::TlsCryptoEngine>(
+ vespalib::test::make_telemetry_only_capability_tls_options_for_testing());
return std::make_shared<vespalib::MaybeTlsCryptoEngine>(std::move(tls), true);
} else if (engine == "tls_maybe_no") {
fprintf(stderr, "crypto engine: null client, mixed server\n");
- auto tls = std::make_shared<vespalib::TlsCryptoEngine>(vespalib::test::make_tls_options_for_testing());
+ auto tls = std::make_shared<vespalib::TlsCryptoEngine>(
+ vespalib::test::make_telemetry_only_capability_tls_options_for_testing());
return std::make_shared<vespalib::MaybeTlsCryptoEngine>(std::move(tls), false);
}
TEST_FATAL(("invalid crypto engine: " + engine).c_str());
diff --git a/fnet/src/vespa/fnet/frt/require_capabilities.cpp b/fnet/src/vespa/fnet/frt/require_capabilities.cpp
index fc64621717f..df6857625cd 100644
--- a/fnet/src/vespa/fnet/frt/require_capabilities.cpp
+++ b/fnet/src/vespa/fnet/frt/require_capabilities.cpp
@@ -4,6 +4,7 @@
#include "rpcrequest.h"
#include <vespa/fnet/connection.h>
#include <vespa/vespalib/net/connection_auth_context.h>
+#include <vespa/vespalib/net/tls/capability_env_config.h>
#include <vespa/log/bufferedlogger.h>
LOG_SETUP(".fnet.frt.require_capabilities");
@@ -15,15 +16,22 @@ FRT_RequireCapabilities::allow(FRT_RPCRequest& req) const noexcept
{
const auto& auth_ctx = req.GetConnection()->auth_context();
const bool is_authorized = auth_ctx.capabilities().contains_all(_required_capabilities);
- if (!is_authorized) {
+ if (is_authorized) {
+ return true;
+ } else {
+ const auto mode = capability_enforcement_mode_from_env();
+ if (mode == CapabilityEnforcementMode::Disable) {
+ return true;
+ }
auto peer_spec = req.GetConnection()->GetPeerSpec();
std::string method_name(req.GetMethodName(), req.GetMethodNameLen());
- LOGBT(warning, peer_spec, "Permission denied for RPC method '%s'. "
+ LOGBT(warning, peer_spec, "%sPermission denied for RPC method '%s'. "
"Peer at %s with %s. Call requires %s, but peer has %s",
+ ((mode == CapabilityEnforcementMode::LogOnly) ? "(Dry-run only, not enforced): " : ""),
method_name.c_str(), peer_spec.c_str(),
to_string(auth_ctx.peer_credentials()).c_str(),
_required_capabilities.to_string().c_str(),
auth_ctx.capabilities().to_string().c_str());
+ return (mode == CapabilityEnforcementMode::Enforce) ? false : true;
}
- return is_authorized;
}
diff --git a/vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt b/vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt
index 5be2e0d4387..5e91f682f36 100644
--- a/vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt
+++ b/vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt
@@ -4,6 +4,7 @@ vespa_add_library(vespalib_vespalib_net_tls OBJECT
authorization_mode.cpp
auto_reloading_tls_crypto_engine.cpp
capability.cpp
+ capability_env_config.cpp
capability_set.cpp
crypto_codec.cpp
crypto_codec_adapter.cpp
diff --git a/vespalib/src/vespa/vespalib/net/tls/capability_env_config.cpp b/vespalib/src/vespa/vespalib/net/tls/capability_env_config.cpp
new file mode 100644
index 00000000000..ec97882ca64
--- /dev/null
+++ b/vespalib/src/vespa/vespalib/net/tls/capability_env_config.cpp
@@ -0,0 +1,46 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include "capability_env_config.h"
+#include <vespa/vespalib/stllike/string.h>
+#include <cstdlib>
+
+#include <vespa/log/log.h>
+LOG_SETUP(".vespalib.net.tls.capability_env_config");
+
+namespace vespalib::net::tls {
+
+namespace {
+
+CapabilityEnforcementMode parse_enforcement_mode_from_env() noexcept {
+ const char* env = getenv("VESPA_TLS_CAPABILITIES_ENFORCEMENT_MODE");
+ vespalib::string mode = env ? env : "";
+ if (mode == "enforce") {
+ return CapabilityEnforcementMode::Enforce;
+ } else if (mode == "log_only") {
+ return CapabilityEnforcementMode::LogOnly;
+ } else if (mode == "disable") {
+ return CapabilityEnforcementMode::Disable;
+ } else if (!mode.empty()) {
+ LOG(warning, "VESPA_TLS_CAPABILITIES_ENFORCEMENT_MODE environment variable has "
+ "an unsupported value (%s). Falling back to 'enforce'", mode.c_str());
+ }
+ return CapabilityEnforcementMode::Enforce;
+}
+
+}
+
+const char* to_string(CapabilityEnforcementMode mode) noexcept {
+ switch (mode) {
+ case CapabilityEnforcementMode::Enforce: return "Enforce";
+ case CapabilityEnforcementMode::LogOnly: return "LogOnly";
+ case CapabilityEnforcementMode::Disable: return "Disable";
+ default: abort();
+ }
+}
+
+CapabilityEnforcementMode capability_enforcement_mode_from_env() noexcept {
+ static const CapabilityEnforcementMode mode = parse_enforcement_mode_from_env();
+ return mode;
+}
+
+}
diff --git a/vespalib/src/vespa/vespalib/net/tls/capability_env_config.h b/vespalib/src/vespa/vespalib/net/tls/capability_env_config.h
new file mode 100644
index 00000000000..e66fca9656b
--- /dev/null
+++ b/vespalib/src/vespa/vespalib/net/tls/capability_env_config.h
@@ -0,0 +1,16 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#pragma once
+
+namespace vespalib::net::tls {
+
+enum class CapabilityEnforcementMode {
+ Disable,
+ LogOnly,
+ Enforce
+};
+
+const char* to_string(CapabilityEnforcementMode mode) noexcept;
+
+CapabilityEnforcementMode capability_enforcement_mode_from_env() noexcept;
+
+}
diff --git a/vespalib/src/vespa/vespalib/net/tls/maybe_tls_crypto_socket.cpp b/vespalib/src/vespa/vespalib/net/tls/maybe_tls_crypto_socket.cpp
index be692e8df35..04613cb3a65 100644
--- a/vespalib/src/vespa/vespalib/net/tls/maybe_tls_crypto_socket.cpp
+++ b/vespalib/src/vespa/vespalib/net/tls/maybe_tls_crypto_socket.cpp
@@ -5,6 +5,7 @@
#include "tls_crypto_socket.h"
#include "protocol_snooping.h"
#include <vespa/vespalib/data/smart_buffer.h>
+#include <vespa/vespalib/net/connection_auth_context.h>
#include <vespa/vespalib/util/size_literals.h>
namespace vespalib {
@@ -94,4 +95,8 @@ MaybeTlsCryptoSocket::MaybeTlsCryptoSocket(SocketHandle socket, std::shared_ptr<
{
}
+std::unique_ptr<net::ConnectionAuthContext> MaybeTlsCryptoSocket::make_auth_context() {
+ return _socket->make_auth_context();
+}
+
} // namespace vespalib
diff --git a/vespalib/src/vespa/vespalib/net/tls/maybe_tls_crypto_socket.h b/vespalib/src/vespa/vespalib/net/tls/maybe_tls_crypto_socket.h
index ba706cb3cd7..1928307e5ad 100644
--- a/vespalib/src/vespa/vespalib/net/tls/maybe_tls_crypto_socket.h
+++ b/vespalib/src/vespa/vespalib/net/tls/maybe_tls_crypto_socket.h
@@ -33,6 +33,7 @@ public:
ssize_t flush() override { return _socket->flush(); }
ssize_t half_close() override { return _socket->half_close(); }
void drop_empty_buffers() override { _socket->drop_empty_buffers(); }
+ std::unique_ptr<net::ConnectionAuthContext> make_auth_context() override;
};
} // namespace vespalib
diff --git a/vespalib/src/vespa/vespalib/net/tls/transport_security_options.cpp b/vespalib/src/vespa/vespalib/net/tls/transport_security_options.cpp
index 3b3fdd579d1..f3ae1a05919 100644
--- a/vespalib/src/vespa/vespalib/net/tls/transport_security_options.cpp
+++ b/vespalib/src/vespa/vespalib/net/tls/transport_security_options.cpp
@@ -29,6 +29,15 @@ TransportSecurityOptions::TransportSecurityOptions(vespalib::string ca_certs_pem
{
}
+TransportSecurityOptions::TransportSecurityOptions(const TransportSecurityOptions&) = default;
+TransportSecurityOptions& TransportSecurityOptions::operator=(const TransportSecurityOptions&) = default;
+TransportSecurityOptions::TransportSecurityOptions(TransportSecurityOptions&&) noexcept = default;
+TransportSecurityOptions& TransportSecurityOptions::operator=(TransportSecurityOptions&&) noexcept = default;
+
+TransportSecurityOptions::~TransportSecurityOptions() {
+ secure_memzero(&_private_key_pem[0], _private_key_pem.size());
+}
+
TransportSecurityOptions TransportSecurityOptions::copy_without_private_key() const {
return TransportSecurityOptions(_ca_certs_pem, _cert_chain_pem, "",
_authorized_peers, _disable_hostname_validation);
@@ -62,8 +71,4 @@ TransportSecurityOptions::Params::Params(Params&&) noexcept = default;
TransportSecurityOptions::Params&
TransportSecurityOptions::Params::operator=(TransportSecurityOptions::Params&&) noexcept = default;
-TransportSecurityOptions::~TransportSecurityOptions() {
- secure_memzero(&_private_key_pem[0], _private_key_pem.size());
-}
-
} // vespalib::net::tls
diff --git a/vespalib/src/vespa/vespalib/net/tls/transport_security_options.h b/vespalib/src/vespa/vespalib/net/tls/transport_security_options.h
index 84fc9c1cbbe..4694373671b 100644
--- a/vespalib/src/vespa/vespalib/net/tls/transport_security_options.h
+++ b/vespalib/src/vespa/vespalib/net/tls/transport_security_options.h
@@ -46,6 +46,10 @@ public:
};
explicit TransportSecurityOptions(Params params);
+ TransportSecurityOptions(const TransportSecurityOptions&);
+ TransportSecurityOptions& operator=(const TransportSecurityOptions&);
+ TransportSecurityOptions(TransportSecurityOptions&&) noexcept;
+ TransportSecurityOptions& operator=(TransportSecurityOptions&&) noexcept;
~TransportSecurityOptions();
diff --git a/vespalib/src/vespa/vespalib/test/make_tls_options_for_testing.cpp b/vespalib/src/vespa/vespalib/test/make_tls_options_for_testing.cpp
index 29043dc42a5..1210b1d7c87 100644
--- a/vespalib/src/vespa/vespalib/test/make_tls_options_for_testing.cpp
+++ b/vespalib/src/vespa/vespalib/test/make_tls_options_for_testing.cpp
@@ -1,17 +1,20 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
#include "make_tls_options_for_testing.h"
+#include "peer_policy_utils.h"
#include <vespa/vespalib/crypto/private_key.h>
#include <vespa/vespalib/crypto/x509_certificate.h>
namespace {
using namespace vespalib::crypto;
+using namespace vespalib::net::tls;
struct TransientCryptoCredentials {
CertKeyWrapper root_ca;
CertKeyWrapper host_creds;
vespalib::net::tls::TransportSecurityOptions cached_transport_options;
+ vespalib::net::tls::TransportSecurityOptions cached_constrained_transport_options;
TransientCryptoCredentials();
~TransientCryptoCredentials();
@@ -29,14 +32,16 @@ struct TransientCryptoCredentials {
return {std::move(cert), std::move(key)};
}
- static CertKeyWrapper make_host_creds(const CertKeyWrapper& root_ca_creds) {
+ static CertKeyWrapper make_host_creds(const CertKeyWrapper& root_ca_creds,
+ const vespalib::string& extra_san_entry) {
auto dn = X509Certificate::DistinguishedName()
.country("US").state("CA").locality("Sunnyvale")
.organization("Wile E. Coyote, Ltd.")
.organizational_unit("Unit Testing and Anvil Dropping Division")
.add_common_name("localhost"); // Should technically not be needed, but including it anyway.
auto subject = X509Certificate::SubjectInfo(std::move(dn));
- subject.add_subject_alt_name("DNS:localhost");
+ subject.add_subject_alt_name("DNS:localhost")
+ .add_subject_alt_name(extra_san_entry);
auto key = PrivateKey::generate_p256_ec_key();
auto params = X509Certificate::Params::issued_by(std::move(subject), key, root_ca_creds.cert, root_ca_creds.key);
params.valid_for = std::chrono::hours(1);
@@ -49,12 +54,18 @@ struct TransientCryptoCredentials {
TransientCryptoCredentials::TransientCryptoCredentials()
: root_ca(make_root_ca()),
- host_creds(make_host_creds(root_ca)),
+ host_creds(make_host_creds(root_ca, "DNS:anvils.example")),
cached_transport_options(vespalib::net::tls::TransportSecurityOptions::Params().
ca_certs_pem(root_ca.cert->to_pem()).
cert_chain_pem(host_creds.cert->to_pem()).
private_key_pem(host_creds.key->private_to_pem()).
- authorized_peers(vespalib::net::tls::AuthorizedPeers::allow_all_authenticated()))
+ authorized_peers(vespalib::net::tls::AuthorizedPeers::allow_all_authenticated())),
+ cached_constrained_transport_options(vespalib::net::tls::TransportSecurityOptions::Params().
+ ca_certs_pem(root_ca.cert->to_pem()).
+ cert_chain_pem(host_creds.cert->to_pem()).
+ private_key_pem(host_creds.key->private_to_pem()).
+ authorized_peers(authorized_peers({policy_with({required_san_dns("anvils.example")},
+ CapabilitySet::telemetry())})))
{}
TransientCryptoCredentials::~TransientCryptoCredentials() = default;
@@ -74,4 +85,8 @@ vespalib::net::tls::TransportSecurityOptions make_tls_options_for_testing() {
return TransientCryptoCredentials::instance().cached_transport_options;
}
+vespalib::net::tls::TransportSecurityOptions make_telemetry_only_capability_tls_options_for_testing() {
+ return TransientCryptoCredentials::instance().cached_constrained_transport_options;
+}
+
} // namespace vespalib::test
diff --git a/vespalib/src/vespa/vespalib/test/make_tls_options_for_testing.h b/vespalib/src/vespa/vespalib/test/make_tls_options_for_testing.h
index 055d9cbdfa6..5a07f796991 100644
--- a/vespalib/src/vespa/vespalib/test/make_tls_options_for_testing.h
+++ b/vespalib/src/vespa/vespalib/test/make_tls_options_for_testing.h
@@ -20,4 +20,12 @@ extern SocketSpec local_spec;
**/
vespalib::net::tls::TransportSecurityOptions make_tls_options_for_testing();
+/**
+ * Make security options whose authz rules only grant the telemetry capability
+ * set to the included certificate.
+ *
+ * Only useful for testing capability propagation and filtering.
+ */
+vespalib::net::tls::TransportSecurityOptions make_telemetry_only_capability_tls_options_for_testing();
+
} // namespace vespalib::test