diff options
author | Tor Brede Vekterli <vekterli@yahooinc.com> | 2022-08-22 13:55:17 +0000 |
---|---|---|
committer | Tor Brede Vekterli <vekterli@yahooinc.com> | 2022-08-22 14:11:59 +0000 |
commit | 55e0f93a74214caf22c4fd79d60e1b0b6836a99c (patch) | |
tree | 0a59f93c8c46f98007057ab9e49a789867e2018d | |
parent | b4117991e6d98dbc9a93625f6dc7170cdd1484dc (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.
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 |