diff --git a/api/BUILD b/api/BUILD index 47dfa333a2ec2..23217eded4b57 100644 --- a/api/BUILD +++ b/api/BUILD @@ -97,6 +97,7 @@ proto_library( "//contrib/envoy/extensions/regex_engines/hyperscan/v3alpha:pkg", "//contrib/envoy/extensions/router/cluster_specifier/golang/v3alpha:pkg", "//contrib/envoy/extensions/tap_sinks/udp_sink/v3alpha:pkg", + "//contrib/envoy/extensions/transport_sockets/tls/certificate_selectors/dynamic_sds/v3alpha:pkg", "//contrib/envoy/extensions/upstreams/http/tcp/golang/v3alpha:pkg", "//contrib/envoy/extensions/vcl/v3alpha:pkg", "//envoy/admin/v3:pkg", diff --git a/api/contrib/envoy/extensions/transport_sockets/tls/certificate_selectors/dynamic_sds/v3alpha/BUILD b/api/contrib/envoy/extensions/transport_sockets/tls/certificate_selectors/dynamic_sds/v3alpha/BUILD new file mode 100644 index 0000000000000..42da23f289e18 --- /dev/null +++ b/api/contrib/envoy/extensions/transport_sockets/tls/certificate_selectors/dynamic_sds/v3alpha/BUILD @@ -0,0 +1,14 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/config/core/v3:pkg", + "//envoy/type/matcher/v3:pkg", + "@com_github_cncf_xds//udpa/annotations:pkg", + "@com_github_cncf_xds//xds/annotations/v3:pkg", + ], +) diff --git a/api/contrib/envoy/extensions/transport_sockets/tls/certificate_selectors/dynamic_sds/v3alpha/config.proto b/api/contrib/envoy/extensions/transport_sockets/tls/certificate_selectors/dynamic_sds/v3alpha/config.proto new file mode 100644 index 0000000000000..de7fd9e64002e --- /dev/null +++ b/api/contrib/envoy/extensions/transport_sockets/tls/certificate_selectors/dynamic_sds/v3alpha/config.proto @@ -0,0 +1,67 @@ +syntax = "proto3"; + +package envoy.extensions.transport_sockets.tls.certificate_selectors.dynamic_sds.v3alpha; + +import "envoy/config/core/v3/config_source.proto"; +import "envoy/type/matcher/v3/regex.proto"; + +import "google/protobuf/duration.proto"; + +import "xds/annotations/v3/status.proto"; + +import "udpa/annotations/status.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.transport_sockets.tls.certificate_selectors.dynamic_sds.v3alpha"; +option java_outer_classname = "ConfigProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/contrib/envoy/extensions/transport_sockets/tls/certificate_selectors/dynamic_sds/v3alpha"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; +option (xds.annotations.v3.file_status).work_in_progress = true; + +// [#protodoc-title: Dynamic SDS Certificate Selector] +// [#extension: envoy.tls.certificate_selectors.dynamic_sds] + +// Dynamic SDS Certificate Selector Configuration. +// This extension enables dynamic TLS certificate selection during SSL handshake +// based on connection metadata using regex-based matching. It uses individual SDS +// subscription approach and can be enhanced to support collection subscriptions +// for better performance with large certificate counts. +// Statistics are always collected for monitoring and debugging. +message DynamicSdsCertificateSelectorConfig { + // Certificate selection rule that maps regex patterns to certificate names. + message SelectionRule { + // TODO (igadot): [future] other types of selection rules? (e.g. filter state for arbitrary logic in the network filter according to the ip/client hellp?) + // Regular expression pattern to match against the input source. + type.matcher.v3.RegexMatchAndSubstitute sni_value_rewrite = 1 + [(validate.rules).message = {required: true}]; + } + + // SDS configuration source for certificate lookup. + config.core.v3.ConfigSource sds_source = 1 [(validate.rules).message = {required: true}]; + + // Certificate cache configuration to optimize performance. + CacheConfig cache_config = 2; + + // Enable fallback to the default certificate selector. + // This provides a safety net during rollout or when the extension fails. + bool enable_default_selector_fallback = 3; + + // List of selection rules evaluated in order. First matching rule wins. + // If no rules match, the connection will use the default certificate selector fallback if enabled. + repeated SelectionRule rules = 4 [(validate.rules).repeated = {min_items: 1}]; +} + +// Certificate cache configuration for performance optimization. +message CacheConfig { + // Time-to-live for cached certificates. + // Certificates will be evicted from cache after this duration to ensure freshness. + google.protobuf.Duration cache_ttl = 1 [(validate.rules).duration = {gte {seconds: 600}}]; + + // Maximum number of certificates to cache (0 for unlimited) + uint32 max_cache_entries = 2; + + // Cache eviction check interval. + // How frequently to scan for and remove expired cache entries. + google.protobuf.Duration eviction_interval = 3 [(validate.rules).duration = {gte {seconds: 30}}]; +} diff --git a/api/versioning/BUILD b/api/versioning/BUILD index cdcf5f206f24c..19f3a8583e204 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -35,6 +35,7 @@ proto_library( "//contrib/envoy/extensions/regex_engines/hyperscan/v3alpha:pkg", "//contrib/envoy/extensions/router/cluster_specifier/golang/v3alpha:pkg", "//contrib/envoy/extensions/tap_sinks/udp_sink/v3alpha:pkg", + "//contrib/envoy/extensions/transport_sockets/tls/certificate_selectors/dynamic_sds/v3alpha:pkg", "//contrib/envoy/extensions/upstreams/http/tcp/golang/v3alpha:pkg", "//contrib/envoy/extensions/vcl/v3alpha:pkg", "//envoy/admin/v3:pkg", diff --git a/contrib/contrib_build_config.bzl b/contrib/contrib_build_config.bzl index cfdc9a849e11d..2a1d0f2dccb60 100644 --- a/contrib/contrib_build_config.bzl +++ b/contrib/contrib_build_config.bzl @@ -48,6 +48,12 @@ CONTRIB_EXTENSIONS = { "envoy.tls.key_providers.cryptomb": "//contrib/cryptomb/private_key_providers/source:config", "envoy.tls.key_providers.qat": "//contrib/qat/private_key_providers/source:config", + # + # TLS certificate selectors + # + + "envoy.tls.certificate_selectors.dynamic_sds": "//contrib/dynamic_sds_certificate_selector/source:config", + # # Socket interface extensions # diff --git a/contrib/dynamic_sds_certificate_selector/source/BUILD b/contrib/dynamic_sds_certificate_selector/source/BUILD new file mode 100644 index 0000000000000..681be7ffb8525 --- /dev/null +++ b/contrib/dynamic_sds_certificate_selector/source/BUILD @@ -0,0 +1,48 @@ +load("//bazel:envoy_build_system.bzl", "envoy_cc_contrib_extension", "envoy_cc_library", "envoy_contrib_package") + +licenses(["notice"]) # Apache 2 + +envoy_contrib_package() + +envoy_cc_library( + name = "dynamic_sds_certificate_selector", + srcs = [ + "certificate_cache.cc", + "dynamic_sds_certificate_selector.cc", + ], + hdrs = [ + "certificate_cache.h", + "dummy_transport_socket_factory_context.h", + "dynamic_sds_certificate_selector.h", + "server_context_config_adapter.h", + "stats.h", + ], + deps = [ + "//envoy/event:dispatcher_interface", + "//envoy/init:manager_interface", + "//envoy/secret:secret_manager_interface", + "//envoy/server:factory_context_interface", + "//envoy/ssl:handshaker_interface", + "//envoy/stats:stats_interface", + "//envoy/stats:stats_macros", + "//source/common/common:assert_lib", + "//source/common/common:logger_lib", + "//source/common/secret:secret_manager_impl_lib", + "//source/common/tls:server_context_lib", + "//source/common/tls:utility_lib", + "@envoy_api//contrib/envoy/extensions/transport_sockets/tls/certificate_selectors/dynamic_sds/v3alpha:pkg_cc_proto", + "@envoy_api//envoy/type/matcher/v3:pkg_cc_proto", + ], +) + +envoy_cc_contrib_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + deps = [ + ":dynamic_sds_certificate_selector", + "//envoy/registry", + "//envoy/ssl:handshaker_interface", + "@envoy_api//contrib/envoy/extensions/transport_sockets/tls/certificate_selectors/dynamic_sds/v3alpha:pkg_cc_proto", + ], +) diff --git a/contrib/dynamic_sds_certificate_selector/source/certificate_cache.cc b/contrib/dynamic_sds_certificate_selector/source/certificate_cache.cc new file mode 100644 index 0000000000000..05fc538e01be7 --- /dev/null +++ b/contrib/dynamic_sds_certificate_selector/source/certificate_cache.cc @@ -0,0 +1,122 @@ +#include "contrib/dynamic_sds_certificate_selector/source/certificate_cache.h" + +#include +#include + +#include "envoy/ssl/context.h" + +#include "source/common/common/assert.h" +#include "source/common/common/fmt.h" + +#include "contrib/dynamic_sds_certificate_selector/source/stats.h" + +namespace Envoy { +namespace Extensions { +namespace TransportSockets { +namespace Tls { +namespace CertificateSelectors { +namespace DynamicSds { + +CertificateCache::CertificateCache(Event::Dispatcher& dispatcher, SelectorStats& stats, + const CacheConfig& config, TimeSource& time_source) + : dispatcher_(dispatcher), stats_(stats), config_(config), time_source_(time_source) { + + UNREFERENCED_PARAMETER(dispatcher_); + /* + Disabling timer since we don't want to evict from cache without removing the provider (it will + reinsert immedietly) + TODO(igadot): implement cache eviction + figure out how to deal with updates (replacing the cert + but not deleting the provider) + + eviction_timer_ = dispatcher_.createTimer([this]() { + evictExpiredEntries(); + scheduleEviction(); + }); + + scheduleEviction(); + */ + + ENVOY_LOG(debug, + "Certificate cache initialized with max_entries={}, ttl={}s, eviction_interval={}s", + config_.max_cache_entries(), config_.cache_ttl().seconds(), + config_.eviction_interval().seconds()); +} + +CertificateCache::~CertificateCache() { + if (eviction_timer_) { + eviction_timer_->disableTimer(); + } +} + +CertificateEntryOptRef CertificateCache::getCertificate(const std::string& selection_key) { + CertificateEntry* entry = nullptr; + { + const auto ts = time_source_.monotonicTime(); + absl::ReaderMutexLock lock(&cache_mu_); + auto it = cache_.find(selection_key); + if (it != cache_.end()) { + entry = it->second.get(); + entry->last_access.store(ts); + } + } + return makeOptRefFromPtr(entry); +} + +void CertificateCache::putCertificate(const std::string& selection_key, + std::unique_ptr&& server_context_impl) { + ASSERT_IS_MAIN_OR_TEST_THREAD(); + if (config_.max_cache_entries() > 0) { + absl::ReaderMutexLock lock(&cache_mu_); + if (cache_.size() >= config_.max_cache_entries() && + cache_.find(selection_key) == cache_.end()) { + ENVOY_LOG(warn, "Certificate cache full - cannot cache certificate for key: {}", + selection_key); + return; + } + } + auto entry = std::make_unique(std::move(server_context_impl), + time_source_.monotonicTime()); + size_t size; + { + absl::WriterMutexLock lock(&cache_mu_); + cache_[selection_key] = std::move(entry); + size = cache_.size(); + } + stats_.cache_size_.set(size); + ENVOY_LOG(debug, "Certificate cached for key: {}, cache size: {}", selection_key, size); +} + +void CertificateCache::evictExpiredEntries() { + auto now = time_source_.monotonicTime(); + auto ttl = Seconds(config_.cache_ttl().seconds()); + + // TODO (igadot): use read lock for checking conditions, and write lock for erasing + absl::WriterMutexLock lock(&cache_mu_); + + auto it = cache_.begin(); + while (it != cache_.end()) { + if (now - it->second->last_access.load() > ttl) { + ENVOY_LOG(debug, "Evicting expired certificate cache entry for key: {}", it->first); + // TODO (igadot): delete the cert provider? (this means the entry here should actually include + // the provider) + cache_.erase(it++); + stats_.cache_evictions_ttl_.inc(); + } else { + ++it; + } + } + + stats_.cache_size_.set(cache_.size()); +} + +void CertificateCache::scheduleEviction() { + auto interval = std::chrono::milliseconds(config_.eviction_interval().seconds() * 1000); + eviction_timer_->enableTimer(interval); +} + +} // namespace DynamicSds +} // namespace CertificateSelectors +} // namespace Tls +} // namespace TransportSockets +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/dynamic_sds_certificate_selector/source/certificate_cache.h b/contrib/dynamic_sds_certificate_selector/source/certificate_cache.h new file mode 100644 index 0000000000000..92ebbf455bb2b --- /dev/null +++ b/contrib/dynamic_sds_certificate_selector/source/certificate_cache.h @@ -0,0 +1,99 @@ +#pragma once + +#include +#include +#include + +#include "envoy/common/optref.h" +#include "envoy/event/dispatcher.h" +#include "envoy/secret/secret_manager.h" +#include "envoy/ssl/context.h" +#include "envoy/ssl/handshaker.h" +#include "envoy/stats/scope.h" + +#include "source/common/common/logger.h" +#include "source/common/tls/server_context_impl.h" + +#include "absl/container/flat_hash_map.h" +#include "absl/types/optional.h" +#include "contrib/dynamic_sds_certificate_selector/source/stats.h" +#include "contrib/envoy/extensions/transport_sockets/tls/certificate_selectors/dynamic_sds/v3alpha/config.pb.h" + +namespace Envoy { +namespace Extensions { +namespace TransportSockets { +namespace Tls { +namespace CertificateSelectors { +namespace DynamicSds { + +using CacheConfig = envoy::extensions::transport_sockets::tls::certificate_selectors::dynamic_sds:: + v3alpha::CacheConfig; + +/** + * Certificate cache entry representing a cached TLS certificate and its metadata. + */ +struct CertificateEntry { + // The actual SSL context ready for use in TLS handshake + const std::unique_ptr server_context_impl; + + // Timestamp when this certificate was cached + const MonotonicTime created_at; + + // Timestamp when this entry was last accessed for LRU eviction + mutable std::atomic last_access; + + CertificateEntry(std::unique_ptr server_context_impl, MonotonicTime created_at) + : server_context_impl(std::move(server_context_impl)), created_at(created_at), + last_access(created_at) {} +}; + +using CertificateEntryOptRef = OptRef; + +/** + * Certificate cache for storing and managing dynamically fetched certificates. + */ +class CertificateCache : Logger::Loggable { +public: + CertificateCache(Event::Dispatcher& dispatcher, SelectorStats& stats, const CacheConfig& config, + TimeSource& time_source); + + ~CertificateCache(); + + /** + * Retrieve a certificate by selection key (e.g., SNI, client IP). + * Updates access statistics for LRU management. + * @param selection_key The key used to identify the certificate + * @return Optional reference to the TLS context if found + */ + CertificateEntryOptRef getCertificate(const std::string& selection_key); + + /** + * Store a certificate in the cache with the given selection key. + * May trigger eviction if cache is full. + * @param selection_key The key to associate with this certificate + * @param server_context_impl The server context that holds the Ssl::TlsCtx with this certificate + */ + void putCertificate(const std::string& selection_key, + std::unique_ptr&& server_context_impl); + +private: + void evictExpiredEntries(); + void scheduleEviction(); + + Event::Dispatcher& dispatcher_; + SelectorStats& stats_; + const CacheConfig config_; + TimeSource& time_source_; + absl::Mutex cache_mu_; + absl::flat_hash_map> + cache_ ABSL_GUARDED_BY(cache_mu_); + // Timer for periodic eviction of expired entries + Event::TimerPtr eviction_timer_; +}; + +} // namespace DynamicSds +} // namespace CertificateSelectors +} // namespace Tls +} // namespace TransportSockets +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/dynamic_sds_certificate_selector/source/config.cc b/contrib/dynamic_sds_certificate_selector/source/config.cc new file mode 100644 index 0000000000000..bcca0441eeb9f --- /dev/null +++ b/contrib/dynamic_sds_certificate_selector/source/config.cc @@ -0,0 +1,96 @@ +#include "contrib/dynamic_sds_certificate_selector/source/config.h" + +#include + +#include "envoy/registry/registry.h" +#include "envoy/ssl/handshaker.h" + +#include "source/common/common/assert.h" +#include "source/common/protobuf/message_validator_impl.h" +#include "source/common/protobuf/utility.h" +#include "source/common/tls/default_tls_certificate_selector.h" + +#include "contrib/dynamic_sds_certificate_selector/source/dynamic_sds_certificate_selector.h" +#include "contrib/envoy/extensions/transport_sockets/tls/certificate_selectors/dynamic_sds/v3alpha/config.pb.validate.h" // IWYU pragma: keep + +namespace Envoy { +namespace Extensions { +namespace TransportSockets { +namespace Tls { +namespace CertificateSelectors { +namespace DynamicSds { + +Ssl::TlsCertificateSelectorFactory +DynamicSdsCertificateSelectorConfigFactory::createTlsCertificateSelectorFactory( + const Protobuf::Message& config, Server::Configuration::CommonFactoryContext& factory_context, + ProtobufMessage::ValidationVisitor& validation_visitor, absl::Status& creation_status, + bool for_quic) { + + // Default selector fallback - also used if the config is bad + auto default_config_factory = + TlsCertificateSelectorConfigFactoryImpl::getDefaultTlsCertificateSelectorConfigFactory(); + const Protobuf::Any any; + auto default_selector_factory = default_config_factory->createTlsCertificateSelectorFactory( + any, factory_context, ProtobufMessage::getNullValidationVisitor(), creation_status, for_quic); + + if (for_quic) { + // QUIC does not support async certificate selection + ENVOY_LOG(error, "Dynamic SDS Certificate Selector does not support QUIC"); + return default_selector_factory; + } + DynamicSdsCertificateSelectorConfig typed_config; + try { + const auto& any_config = dynamic_cast(config); + typed_config = MessageUtil::anyConvertAndValidate( + any_config, validation_visitor); + } catch (EnvoyException& e) { + ENVOY_LOG(error, "Invalid DynamicSdsCertificateSelectorConfig: {}", e.what()); + return default_selector_factory; + } catch (std::bad_cast& e) { + ENVOY_LOG(error, "Invalid proto type of {}, details: {}", config.GetTypeName(), e.what()); + return default_selector_factory; + } + + // Compile regex rules during validation + auto rules_status = DynamicSdsCertificateSelector::compileRules(typed_config); + if (!rules_status.ok()) { + ENVOY_LOG(error, "Invalid selection rules: {}", rules_status.status().message()); + return default_selector_factory; + } + + // Capture factory context resources that we need + // The provided factory context is actually a ServerFactoryContext (not CommonFactoryContext) + ASSERT(dynamic_cast(&factory_context) != nullptr); + auto& server_factory_context = + dynamic_cast(factory_context); + auto& secret_manager = server_factory_context.secretManager(); + auto& dispatcher = factory_context.mainThreadDispatcher(); + auto& scope = factory_context.scope(); + + // Return factory lambda that creates the selector instance + // we recompile the rules in the c'tor of the selector becaue of unique_ptr lifetime issues + // the lifetime of the factory is guranteed to be at least the lifetime of any instace created + // from it. + return [typed_config, &server_factory_context, default_selector_factory, &secret_manager, + &dispatcher, &scope](const Ssl::ServerContextConfig& server_context_config, + Ssl::TlsCertificateSelectorContext& selector_ctx) + -> std::unique_ptr { + return std::make_unique( + typed_config, server_factory_context, default_selector_factory, server_context_config, + selector_ctx, secret_manager, dispatcher, scope); + }; +} + +/** + * Static registration for the Dynamic SDS Certificate Selector factory. + * This makes the extension discoverable by Envoy's extension registry. + */ +REGISTER_FACTORY(DynamicSdsCertificateSelectorConfigFactory, + Ssl::TlsCertificateSelectorConfigFactory); + +} // namespace DynamicSds +} // namespace CertificateSelectors +} // namespace Tls +} // namespace TransportSockets +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/dynamic_sds_certificate_selector/source/config.h b/contrib/dynamic_sds_certificate_selector/source/config.h new file mode 100644 index 0000000000000..55db6d7c0a054 --- /dev/null +++ b/contrib/dynamic_sds_certificate_selector/source/config.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include + +#include "envoy/server/factory_context.h" +#include "envoy/ssl/handshaker.h" + +#include "source/common/common/logger.h" + +#include "absl/status/status.h" +#include "contrib/envoy/extensions/transport_sockets/tls/certificate_selectors/dynamic_sds/v3alpha/config.pb.h" + +namespace Envoy { +namespace Extensions { +namespace TransportSockets { +namespace Tls { +namespace CertificateSelectors { +namespace DynamicSds { + +using DynamicSdsCertificateSelectorConfig = envoy::extensions::transport_sockets::tls:: + certificate_selectors::dynamic_sds::v3alpha::DynamicSdsCertificateSelectorConfig; + +/** + * Config factory for Dynamic SDS Certificate Selector. + */ +class DynamicSdsCertificateSelectorConfigFactory : public Ssl::TlsCertificateSelectorConfigFactory, + Logger::Loggable { +public: + // Ssl::TlsCertificateSelectorConfigFactory + Ssl::TlsCertificateSelectorFactory + createTlsCertificateSelectorFactory(const Protobuf::Message& config, + Server::Configuration::CommonFactoryContext& factory_context, + ProtobufMessage::ValidationVisitor& validation_visitor, + absl::Status& creation_status, bool for_quic) override; + + std::string name() const override { return "envoy.tls.certificate_selectors.dynamic_sds"; } + + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } +}; + +DECLARE_FACTORY(DynamicSdsCertificateSelectorConfigFactory); + +} // namespace DynamicSds +} // namespace CertificateSelectors +} // namespace Tls +} // namespace TransportSockets +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/dynamic_sds_certificate_selector/source/dummy_transport_socket_factory_context.h b/contrib/dynamic_sds_certificate_selector/source/dummy_transport_socket_factory_context.h new file mode 100644 index 0000000000000..a2b1cf8bca393 --- /dev/null +++ b/contrib/dynamic_sds_certificate_selector/source/dummy_transport_socket_factory_context.h @@ -0,0 +1,44 @@ +#pragma once + +#include "envoy/common/exception.h" +#include "envoy/server/transport_socket_config.h" + +namespace Envoy { +namespace Extensions { +namespace TransportSockets { +namespace Tls { +namespace CertificateSelectors { +namespace DynamicSds { + +#define DUMMY_METHOD(method_name) \ + auto method_name() \ + -> decltype(Server::Configuration::TransportSocketFactoryContext::method_name()) override { \ + throw EnvoyException("DummyTransportSocketFactoryContext::" #method_name \ + " should not be called"); \ + } + +/** + * Dummy adapter that wraps a ServerFactoryContext and provides a minimal + * TransportSocketFactoryContext interface. This is used when we need to pass + * a TransportSocketFactoryContext but don't actually need the transport socket + * factory functionality (only the ServerFactoryContext). + * + * All methods will throw an EnvoyException. + */ +class DummyTransportSocketFactoryContext + : public Server::Configuration::TransportSocketFactoryContext { +public: + // All other methods should not be called - add assertions + DUMMY_METHOD(serverFactoryContext); + DUMMY_METHOD(messageValidationVisitor); + DUMMY_METHOD(initManager); + DUMMY_METHOD(scope); + DUMMY_METHOD(statsScope); +}; + +} // namespace DynamicSds +} // namespace CertificateSelectors +} // namespace Tls +} // namespace TransportSockets +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/dynamic_sds_certificate_selector/source/dynamic_sds_certificate_selector.cc b/contrib/dynamic_sds_certificate_selector/source/dynamic_sds_certificate_selector.cc new file mode 100644 index 0000000000000..42f7df7aa1737 --- /dev/null +++ b/contrib/dynamic_sds_certificate_selector/source/dynamic_sds_certificate_selector.cc @@ -0,0 +1,393 @@ +#include "contrib/dynamic_sds_certificate_selector/source/dynamic_sds_certificate_selector.h" + +#include +#include + +#include "envoy/ssl/context.h" +#include "envoy/type/matcher/v3/regex.pb.h" + +#include "source/common/common/assert.h" +#include "source/common/common/regex.h" +#include "source/common/init/manager_impl.h" +#include "source/common/ssl/tls_certificate_config_impl.h" +#include "source/common/tls/default_tls_certificate_selector.h" + +#include "contrib/dynamic_sds_certificate_selector/source/certificate_cache.h" +#include "contrib/dynamic_sds_certificate_selector/source/dummy_transport_socket_factory_context.h" +#include "contrib/dynamic_sds_certificate_selector/source/server_context_config_adapter.h" + +namespace Envoy { +namespace Extensions { +namespace TransportSockets { +namespace Tls { +namespace CertificateSelectors { +namespace DynamicSds { + +DynamicSdsCertificateSelector::DynamicSdsCertificateSelector( + const DynamicSdsCertificateSelectorConfig& config, + Server::Configuration::ServerFactoryContext& server_factory_context, + Ssl::TlsCertificateSelectorFactory default_selector_factory, + const Ssl::ServerContextConfig& server_config, Ssl::TlsCertificateSelectorContext& selector_ctx, + Secret::SecretManager& secret_manager, Event::Dispatcher& mt_dispatcher, Stats::Scope& scope) + : config_(config), server_factory_context_(server_factory_context), + server_config_(server_config), server_ctx_(dynamic_cast(selector_ctx)), + secret_manager_(secret_manager), mt_dispatcher_(mt_dispatcher), scope_(scope), + stats_(generateSelectorStats(scope)) { + + // Initialize certificate cache + certificate_cache_ = std::make_unique( + mt_dispatcher, stats_, config.cache_config(), server_factory_context.timeSource()); + + // Create fallback selector if enabled + if (config_.enable_default_selector_fallback()) { + fallback_selector_ = default_selector_factory(server_config, selector_ctx); + } + + auto rules_status = DynamicSdsCertificateSelector::compileRules(config); + if (rules_status.ok()) { + compiled_rules_ = std::move(rules_status.value()); + } else { + IS_ENVOY_BUG(fmt::format("Passed invalid selection rules config to selector: {}", + rules_status.status().message())); + } + + ENVOY_LOG(info, + "Dynamic SDS Certificate Selector initialized with {} compiled rules, cache_ttl={}s", + compiled_rules_.size(), config_.cache_config().cache_ttl().seconds()); +} + +Ssl::SelectionResult +DynamicSdsCertificateSelector::selectTlsContext(const SSL_CLIENT_HELLO& ssl_client_hello, + Ssl::CertificateSelectionCallbackPtr cb) { + + // Extract SNI from ClientHello (rules are SNI-based) + const char* sni_ptr = SSL_get_servername(ssl_client_hello.ssl, TLSEXT_NAMETYPE_host_name); + if (sni_ptr == nullptr) { + ENVOY_LOG(debug, "No SNI in ClientHello, trying fallback"); + return delegateToDefaultSelector(ssl_client_hello, std::move(cb)); + } + + std::string sni_value(sni_ptr); + + // Find matching certificate using regex rules + // TODO (igadot): [future] currently all of our certs are RSA, which all clients support - to add + // ECDSA support, need to findMatchingCertificate according to client capabilities (see + // default_tls_certificate_selector.cc) + std::string cert_name = findMatchingCertificate(sni_value); + + if (cert_name.empty()) { + stats_.invalid_selection_keys_.inc(); + ENVOY_LOG(debug, "No matching certificate for SNI: {}, trying fallback", sni_value); + return delegateToDefaultSelector(ssl_client_hello, std::move(cb)); + } + + ENVOY_LOG(trace, "Certificate selection for SNI: {} -> cert: {}", sni_value, cert_name); + const bool client_ocsp_capable = server_ctx_.isClientOcspCapable(ssl_client_hello); + auto& ssl_stats = server_ctx_.stats(); + if (client_ocsp_capable) { + ssl_stats.ocsp_staple_requests_.inc(); + } + + auto cached_context = certificate_cache_->getCertificate(cert_name); + if (cached_context.has_value()) { + stats_.cache_hits_.inc(); + + ENVOY_LOG(trace, "Certificate selection cache hit for cert: {}", cert_name); + + // Handle OCSP stapling for cached certificate + const auto& tls_context = cached_context->server_context_impl->getTlsContexts()[0]; + auto ocsp_action = ocspStapleAction(tls_context, client_ocsp_capable, ssl_stats); + + if (ocsp_action == Ssl::OcspStapleAction::Fail) { + ENVOY_LOG(debug, "OCSP staple policy failure for cert: {}", cert_name); + return {Ssl::SelectionResult::SelectionStatus::Failed, nullptr, false}; + } + return {Ssl::SelectionResult::SelectionStatus::Success, &tls_context, + ocsp_action == Ssl::OcspStapleAction::Staple}; + } + stats_.cache_misses_.inc(); + + // Cache miss - initiate async fetch + stats_.async_selections_.inc(); + stats_.pending_async_selections_.inc(); + + auto start_time = server_factory_context_.timeSource().monotonicTime(); + + // This is the logic for continuing the handshake back on this thread + // liveness check will happen before invoking the callback back on this thread + auto cert_ready_cb = [this, client_ocsp_capable, cert_name, + start_time](Ssl::CertificateSelectionCallbackPtr cb, + OptRef tls_ctx) { + stats_.pending_async_selections_.dec(); + auto duration = std::chrono::duration_cast( + server_factory_context_.timeSource().monotonicTime() - start_time); + stats_.selection_latency_.recordValue(duration.count()); + + if (!tls_ctx.has_value()) { + ENVOY_LOG(debug, "Empty TLS context for cert: {}", cert_name); + cb->onCertificateSelectionResult({}, false); + return; + } + auto ocsp_action = ocspStapleAction(*tls_ctx, client_ocsp_capable, server_ctx_.stats()); + if (ocsp_action == Ssl::OcspStapleAction::Fail) { + ENVOY_LOG(debug, "OCSP staple policy failure for cert: {}", cert_name); + cb->onCertificateSelectionResult({}, false); + return; + } + cb->onCertificateSelectionResult(tls_ctx, ocsp_action == Ssl::OcspStapleAction::Staple); + }; + + auto mt_cb = std::make_unique(std::move(cb), std::move(cert_ready_cb), + std::weak_ptr(still_alive_)); + + mt_dispatcher_.post([this, cert_name, mt_cb = std::move(mt_cb)]() mutable { + // The selector may have been destroyed (only happens if the handshake is cancelled) + if (mt_cb->expired()) { + return; + } + fetchCertificateAsync(cert_name, std::move(mt_cb)); + }); + + ENVOY_LOG(trace, "Certificate selection pending for cert: {}", cert_name); + return {Ssl::SelectionResult::SelectionStatus::Pending, nullptr, false}; +} + +std::string +DynamicSdsCertificateSelector::findMatchingCertificate(const std::string& sni_value) const { + for (const auto& rule : compiled_rules_) { + if (rule->matcher->match(sni_value)) { + // Apply substitution to get certificate name + std::string cert_name = rule->matcher->replaceAll(sni_value, rule->substitution); + ENVOY_LOG(trace, "SNI '{}' matches pattern, using certificate: {}", sni_value, cert_name); + return cert_name; + } + } + + ENVOY_LOG(trace, "No matching certificate rule found for SNI: {}", sni_value); + return EMPTY_STRING; +} + +void DynamicSdsCertificateSelector::fetchCertificateAsync(const std::string& cert_name, + DynamicSdsCallbackPtr cb) { + ASSERT_IS_MAIN_OR_TEST_THREAD(); + + auto [it, already_exists] = pending_selections_.try_emplace(cert_name); + it->second.push_back(std::move(cb)); + if (!already_exists) { + ENVOY_LOG(trace, "Added to pending selection queue for cert: {}", cert_name); + return; + } + + // We may already have the certificate in the cache - e.g. if a new connection arrives during + // handling of a callback in the main thread + auto cached_context = certificate_cache_->getCertificate(cert_name); + if (cached_context.has_value()) { + ENVOY_LOG(trace, "main thread: Certificate already cached for cert: {}", cert_name); + onCertificateReady(cert_name, + makeOptRef(cached_context->server_context_impl->getTlsContexts()[0])); + return; + } + + // If a provider exists, either there are pending selections, or the certificate is in the cache. + // The provider should always be destroyed before a certificate is removed from the cache + ASSERT(!sds_providers_.contains(cert_name), + fmt::format("provider for cert {} already exists", cert_name)); + + // Use a per-secret init manager so handshake continues on update failure (see + // SdsApi::onConfigUpdate* methods) e.g. when xDS server returns an empty response - we want to + // remove the provider + auto init_manager = std::make_unique(cert_name); + auto provider = secret_manager_.findOrCreateTlsCertificateProvider( + config_.sds_source(), cert_name, server_factory_context_, *init_manager); + + // See ContextConfigImpl::setSecretUpdateCallback for inspiration + // Called every time the secret is updated + auto handle = provider->addUpdateCallback([this, cert_name, provider]() -> absl::Status { + auto tls_cert = provider->secret(); + ASSERT(tls_cert != nullptr); // see SdsApi::onConfigUpdate, should never be null + if (tls_cert->has_private_key_provider()) { + return absl::UnimplementedError( + "On-demand certificates with private key provider are not supported"); + } + // Create a dummy TransportSocketFactoryContext wrapper since TlsCertificateConfigImpl requires + // it We checked above that there are no private key providers, so the transport socket + // functionality won't be used + static auto dummy_transport_context = DummyTransportSocketFactoryContext(); + auto config_or_error = Ssl::TlsCertificateConfigImpl::create( + *tls_cert, dummy_transport_context, server_factory_context_.api(), cert_name); + if (auto& status = config_or_error.status(); !status.ok()) { + ENVOY_LOG(error, "Failed to create TlsCertificateConfig for cert: {}", cert_name); + return status; + } + + // Create a lightweight adapter config that wraps only the specific TLS certificate + // This allows us to create an isolated ServerContext for this certificate + auto adapter_config = ServerContextConfigAdapter(config_or_error.value(), server_config_); + + // Create ServerContextImpl using the adapter config + // server_names is used for session resumption hash - use the secret name instead + auto ctx_or_error = ServerContextImpl::create(scope_, std::move(adapter_config), {cert_name}, + server_factory_context_, nullptr); + if (auto& status = ctx_or_error.status(); !status.ok()) { + ENVOY_LOG(error, "Failed to create ServerContextImpl for cert: {}", cert_name); + return status; + } + + auto& server_context_impl = ctx_or_error.value(); + + ASSERT(server_context_impl->getTlsContexts().size() == 1); + certificate_cache_->putCertificate(cert_name, std::move(server_context_impl)); + return absl::OkStatus(); + }); + + // logic for when the secret fetch is completed for the first time (called after the update + // callback above) + auto cert_ready_watcher = std::make_unique(cert_name, [this, cert_name] { + auto cached_context = certificate_cache_->getCertificate(cert_name); + if (cached_context.has_value()) { + onCertificateReady(cert_name, {cached_context->server_context_impl->getTlsContexts()[0]}); + } else { + // This means the initial fetch failed - remove the provider since it most likely means cert + // doesn't exist + auto removed = sds_providers_.erase(cert_name); + ASSERT(removed == 1); + // TODO (igadot): [future] think of how to evict outdated secrets from cache (or just TTL) + stats_.sds_errors_.inc(); + onCertificateReady(cert_name, {}); + } + }); + + // This will initiate the SDS subscription + init_manager->initialize(*cert_ready_watcher); + + // we checked earlier that this should succeed + RELEASE_ASSERT( + sds_providers_ + .try_emplace(cert_name, std::make_unique( + std::move(provider), std::move(handle), + std::move(init_manager), std::move(cert_ready_watcher))) + .second, + "sds_providers map already contains cert_name"); +} + +void DynamicSdsCertificateSelector::onCertificateReady(const std::string& cert_name, + OptRef tls_context) { + + auto pending_cbs = pending_selections_.find(cert_name); + if (pending_cbs != pending_selections_.end()) { + for (auto& cb : pending_cbs->second) { + // Each callback dispatches the work to the correct worker + cb->onCertificateReady(tls_context); + } + pending_selections_.erase(pending_cbs); + } +} + +Ssl::SelectionResult +DynamicSdsCertificateSelector::delegateToDefaultSelector(const SSL_CLIENT_HELLO& ssl_client_hello, + Ssl::CertificateSelectionCallbackPtr cb) { + + if (config_.enable_default_selector_fallback()) { + if (fallback_selector_) { + stats_.default_selector_used_.inc(); + ENVOY_LOG(debug, "Delegating to default certificate selector"); + return fallback_selector_->selectTlsContext(ssl_client_hello, std::move(cb)); + } + } + + stats_.selection_failures_.inc(); + return {Ssl::SelectionResult::SelectionStatus::Failed, nullptr, false}; +} + +Ssl::OcspStapleAction DynamicSdsCertificateSelector::ocspStapleAction( + const Ssl::TlsContext& tls_context, bool client_ocsp_capable, SslStats& ssl_stats) const { + + if (!client_ocsp_capable) { + return Ssl::OcspStapleAction::ClientNotCapable; + } + + auto& response = tls_context.ocsp_response_; + + // Get the OCSP staple policy from server config + auto policy = server_config_.ocspStaplePolicy(); + + // Check if the certificate has the must-staple extension - upgrade policy to match + if (tls_context.is_must_staple_) { + policy = Ssl::ServerContextConfig::OcspStaplePolicy::MustStaple; + } + + const bool valid_response = response && !response->isExpired(); + + const auto ocsp_action = [&] { + switch (policy) { + case Ssl::ServerContextConfig::OcspStaplePolicy::LenientStapling: + if (!valid_response) { + return Ssl::OcspStapleAction::NoStaple; + } + return Ssl::OcspStapleAction::Staple; + + case Ssl::ServerContextConfig::OcspStaplePolicy::StrictStapling: + if (valid_response) { + return Ssl::OcspStapleAction::Staple; + } + if (response) { + // Expired response. + return Ssl::OcspStapleAction::Fail; + } + return Ssl::OcspStapleAction::NoStaple; + + case Ssl::ServerContextConfig::OcspStaplePolicy::MustStaple: + if (!valid_response) { + return Ssl::OcspStapleAction::Fail; + } + return Ssl::OcspStapleAction::Staple; + PANIC_DUE_TO_CORRUPT_ENUM; + } + }(); + // Handle OCSP policy failures + + switch (ocsp_action) { + case Ssl::OcspStapleAction::Staple: + ssl_stats.ocsp_staple_responses_.inc(); + break; + case Ssl::OcspStapleAction::NoStaple: + ssl_stats.ocsp_staple_omitted_.inc(); + break; + case Ssl::OcspStapleAction::Fail: + ssl_stats.ocsp_staple_failed_.inc(); + case Ssl::OcspStapleAction::ClientNotCapable: + // Client doesn't support OCSP, no action needed + break; + } + return ocsp_action; +} + +absl::StatusOr>> +DynamicSdsCertificateSelector::compileRules(const DynamicSdsCertificateSelectorConfig& config) { + + std::vector> result{}; + for (const auto& rule : config.rules()) { + const auto& rewrite_config = rule.sni_value_rewrite(); + const auto& pattern = rewrite_config.pattern(); + + // Compile regex pattern + auto regex_matcher = Regex::CompiledGoogleReMatcher::create(pattern); + if (!regex_matcher.ok()) { + return regex_matcher.status(); + } + + // Create compiled rule with matcher and substitution + result.emplace_back(std::make_unique(std::move(regex_matcher.value()), + rewrite_config.substitution())); + } + + ENVOY_LOG(trace, "Dynamic SDS Certificate Selector compiled {} rules", result.size()); + return result; +} + +} // namespace DynamicSds +} // namespace CertificateSelectors +} // namespace Tls +} // namespace TransportSockets +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/dynamic_sds_certificate_selector/source/dynamic_sds_certificate_selector.h b/contrib/dynamic_sds_certificate_selector/source/dynamic_sds_certificate_selector.h new file mode 100644 index 0000000000000..144d93c192622 --- /dev/null +++ b/contrib/dynamic_sds_certificate_selector/source/dynamic_sds_certificate_selector.h @@ -0,0 +1,185 @@ +#pragma once + +#include +#include + +#include "envoy/event/dispatcher.h" +#include "envoy/secret/secret_manager.h" +#include "envoy/secret/secret_provider.h" +#include "envoy/server/factory_context.h" +#include "envoy/ssl/handshaker.h" +#include "envoy/stats/scope.h" + +#include "source/common/common/logger.h" +#include "source/common/common/regex.h" +#include "source/common/init/manager_impl.h" +#include "source/common/init/watcher_impl.h" +#include "source/common/tls/context_impl.h" + +#include "absl/container/flat_hash_map.h" +#include "absl/strings/string_view.h" +#include "contrib/dynamic_sds_certificate_selector/source/certificate_cache.h" +#include "contrib/dynamic_sds_certificate_selector/source/stats.h" +#include "contrib/envoy/extensions/transport_sockets/tls/certificate_selectors/dynamic_sds/v3alpha/config.pb.h" + +namespace Envoy { +namespace Extensions { +namespace TransportSockets { +namespace Tls { +namespace CertificateSelectors { +namespace DynamicSds { + +using DynamicSdsCertificateSelectorConfig = envoy::extensions::transport_sockets::tls:: + certificate_selectors::dynamic_sds::v3alpha::DynamicSdsCertificateSelectorConfig; + +struct DynamicSdsCallback { + + // This is called on the main thread, and immedietly dispatches to worker threads. + void onCertificateReady(OptRef tls_ctx) { + // Capture by value to avoid use-after-free when DynamicSdsCallback is destroyed + // after pending_selections_.erase() but before the lambda executes + cb_->dispatcher().post([still_alive = still_alive_, cb = std::move(cb_), + cert_ready_cb = cert_ready_cb_, tls_ctx]() mutable { + if (still_alive.expired()) { + return; + } + cert_ready_cb(std::move(cb), tls_ctx); + }); + } + + bool expired() { return still_alive_.expired(); } + + Ssl::CertificateSelectionCallbackPtr cb_; + const std::function)> + cert_ready_cb_; + const std::weak_ptr still_alive_; +}; + +using DynamicSdsCallbackPtr = std::unique_ptr; + +struct SdsProviderState { + const Secret::TlsCertificateConfigProviderSharedPtr provider_; + const Common::CallbackHandlePtr handle_; + const std::unique_ptr init_manager_; + const std::unique_ptr watcher_; +}; + +/** + * Compiled certificate selection rule with precompiled regex matcher. + */ +struct CompiledRule { + CompiledRule(std::unique_ptr matcher_arg, + const std::string& substitution) + : matcher(std::move(matcher_arg)), substitution(substitution) {} + + const std::unique_ptr matcher; + const std::string substitution; +}; + +/** + * Dynamic SDS Certificate Selector implementation. + * + * This extension enables dynamic TLS certificate selection during SSL handshake + * based on connection metadata such as SNI. It creates SDS subscriptions on-demand + * and caches certificates for performance. + */ +class DynamicSdsCertificateSelector : public Ssl::TlsCertificateSelector, + Logger::Loggable { +public: + DynamicSdsCertificateSelector(const DynamicSdsCertificateSelectorConfig& config, + Server::Configuration::ServerFactoryContext& server_factory_context, + Ssl::TlsCertificateSelectorFactory default_selector_factory, + const Ssl::ServerContextConfig& server_config, + Ssl::TlsCertificateSelectorContext& selector_ctx, + Secret::SecretManager& secret_manager, + Event::Dispatcher& mt_dispatcher, Stats::Scope& scope); + + Ssl::SelectionResult selectTlsContext(const SSL_CLIENT_HELLO& ssl_client_hello, + Ssl::CertificateSelectionCallbackPtr cb) override; + + std::pair + findTlsContext(absl::string_view, const Ssl::CurveNIDVector&, bool, bool*) override { + RELEASE_ASSERT( + false, + "findTlsContext (QUIC) not implemented, and it should never be called (for_quic == false)"); + } + + /** + * Compile regex patterns from the configuration into reusable matchers. + * This is called during validation to ensure all patterns are valid and to + * pre-compile them for performance during certificate selection. + */ + static absl::StatusOr>> + compileRules(const DynamicSdsCertificateSelectorConfig& config); + +private: + /** + * Find matching certificate name using regex rules. + * Returns the certificate name if a rule matches, empty string otherwise. + * @param sni_value The SNI value to match against rules + */ + std::string findMatchingCertificate(const std::string& sni_value) const; + + /** + * Fetch a certificate asynchronously from SDS and cache it. + * Creates a new SDS subscription if needed. + */ + void fetchCertificateAsync(const std::string& selection_key, DynamicSdsCallbackPtr cb); + + /** + * Transfer back control to the worker thread when the certificate is in the cache. + * Called when SDS udpate completed, and if the secret is valid the TlsContext will be available. + */ + void onCertificateReady(const std::string& selection_key, + OptRef tls_context); + + /** + * Delegate to default selector if enabled and available. + */ + Ssl::SelectionResult delegateToDefaultSelector(const SSL_CLIENT_HELLO& ssl_client_hello, + Ssl::CertificateSelectionCallbackPtr cb); + + /** + * Determine OCSP staple action based on client capabilities, policy, and certificate state. + * Based on DefaultTlsCertificateSelector::ocspStapleAction implementation. + * + * @param tls_context The TLS context with certificate and OCSP response + * @param client_ocsp_capable Whether the client supports OCSP stapling + * @return The appropriate OCSP staple action to take + */ + Ssl::OcspStapleAction ocspStapleAction(const Ssl::TlsContext& tls_context, + bool client_ocsp_capable, SslStats& ssl_stats) const; + + const DynamicSdsCertificateSelectorConfig config_; + std::vector> compiled_rules_{}; + Server::Configuration::ServerFactoryContext& server_factory_context_; + const Ssl::ServerContextConfig& server_config_; + ServerContextImpl& server_ctx_; + Secret::SecretManager& secret_manager_; + Event::Dispatcher& mt_dispatcher_; + Stats::Scope& scope_; + + // Certificate cache for performance optimization + std::unique_ptr certificate_cache_; + + // Default certificate selector for fallback (if enabled) + Ssl::TlsCertificateSelectorPtr fallback_selector_; + + // Pending async selections waiting for certificates + absl::flat_hash_map> pending_selections_; + + // Statistics for monitoring selector performance + SelectorStats stats_; + + // Map selection keys to SDS providers to track active subscriptions and callbacks + absl::flat_hash_map> sds_providers_; + + const std::shared_ptr still_alive_{std::make_shared(true)}; +}; + +} // namespace DynamicSds +} // namespace CertificateSelectors +} // namespace Tls +} // namespace TransportSockets +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/dynamic_sds_certificate_selector/source/server_context_config_adapter.h b/contrib/dynamic_sds_certificate_selector/source/server_context_config_adapter.h new file mode 100644 index 0000000000000..a4b22a51f4cf0 --- /dev/null +++ b/contrib/dynamic_sds_certificate_selector/source/server_context_config_adapter.h @@ -0,0 +1,98 @@ +#pragma once + +#include +#include + +#include "envoy/ssl/context_config.h" +#include "envoy/ssl/handshaker.h" +#include "envoy/ssl/tls_certificate_config.h" + +namespace Envoy { +namespace Extensions { +namespace TransportSockets { +namespace Tls { +namespace CertificateSelectors { +namespace DynamicSds { + +#define DELEGATE_METHOD(method_name) \ + auto method_name() const -> decltype(Ssl::ServerContextConfig::method_name()) override { \ + return base_config_.method_name(); \ + } + +/** + * Lightweight adapter that wraps a single TlsCertificateConfig and provides + * minimal ServerContextConfig interface for creating ServerContext instances. + * This is used to create isolated SSL contexts for individual certificates + * in the dynamic SDS certificate selector. + */ +class ServerContextConfigAdapter : public Ssl::ServerContextConfig { +public: + /** + * Constructor that wraps a TlsCertificateConfig with minimal context. + * @param tls_cert_config The certificate configuration to wrap + * @param base_config Reference ServerContextConfig to delegate most calls to + */ + ServerContextConfigAdapter(Ssl::TlsCertificateConfig& tls_cert_config, + const Ssl::ServerContextConfig& base_config) + : tls_cert_config_(tls_cert_config), base_config_(base_config) {} + + // Return only our single certificate + std::vector> + tlsCertificates() const override { + std::vector> configs; + configs.push_back(tls_cert_config_); + return configs; + } + + void setSecretUpdateCallback(std::function) override { + // No-op: This adapter wraps a static certificate, callbacks are handled by the base config + ASSERT(false); + } + + // Return a no-op factory since the resulting ServerContext is only used for TlsContext objects + Ssl::TlsCertificateSelectorFactory tlsCertificateSelectorFactory() const override { + return [](const Ssl::ServerContextConfig&, + Ssl::TlsCertificateSelectorContext&) -> std::unique_ptr { + return nullptr; + }; + } + + DELEGATE_METHOD(alpnProtocols); + DELEGATE_METHOD(cipherSuites); + DELEGATE_METHOD(ecdhCurves); + DELEGATE_METHOD(signatureAlgorithms); + DELEGATE_METHOD(certificateValidationContext); + DELEGATE_METHOD(minProtocolVersion); + DELEGATE_METHOD(maxProtocolVersion); + DELEGATE_METHOD(isReady); + DELEGATE_METHOD(createHandshaker); + DELEGATE_METHOD(capabilities); + DELEGATE_METHOD(sslctxCb); + DELEGATE_METHOD(tlsKeyLogLocal); + DELEGATE_METHOD(tlsKeyLogRemote); + DELEGATE_METHOD(tlsKeyLogPath); + DELEGATE_METHOD(accessLogManager); + DELEGATE_METHOD(compliancePolicy); + DELEGATE_METHOD(requireClientCertificate); + DELEGATE_METHOD(ocspStaplePolicy); + DELEGATE_METHOD(sessionTicketKeys); + DELEGATE_METHOD(sessionTimeout); + DELEGATE_METHOD(disableStatelessSessionResumption); + DELEGATE_METHOD(disableStatefulSessionResumption); + DELEGATE_METHOD(fullScanCertsOnSNIMismatch); + DELEGATE_METHOD(preferClientCiphers); + +private: + // The wrapped certificate configuration + const Ssl::TlsCertificateConfig& tls_cert_config_; + + // Base configuration to delegate most functionality to + const Ssl::ServerContextConfig& base_config_; +}; + +} // namespace DynamicSds +} // namespace CertificateSelectors +} // namespace Tls +} // namespace TransportSockets +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/dynamic_sds_certificate_selector/source/stats.h b/contrib/dynamic_sds_certificate_selector/source/stats.h new file mode 100644 index 0000000000000..2881e48a5a221 --- /dev/null +++ b/contrib/dynamic_sds_certificate_selector/source/stats.h @@ -0,0 +1,53 @@ +#pragma once + +#include "envoy/stats/scope.h" +#include "envoy/stats/stats_macros.h" + +namespace Envoy { +namespace Extensions { +namespace TransportSockets { +namespace Tls { +namespace CertificateSelectors { +namespace DynamicSds { + +/** + * All stats for the Dynamic SDS Certificate Selector. @see stats_macros.h + */ +#define ALL_DYNAMIC_SDS_SELECTOR_STATS(COUNTER, GAUGE, HISTOGRAM) \ + COUNTER(cache_hits) /* Successful cache lookups */ \ + COUNTER(cache_misses) /* Cache lookup failures */ \ + COUNTER(async_selections) /* Async certificate selections initiated */ \ + COUNTER(default_selector_used) /* Default selector fallback uses */ \ + COUNTER(selection_failures) /* Total selection failures (fallback not used) */ \ + COUNTER(cache_evictions_ttl) /* Evictions due to TTL expiration */ \ + COUNTER(invalid_selection_keys) /* Could not extract selection key from sni */ \ + COUNTER(sds_errors) /* Fetching a certificate from SDS failed (e.g. the certificate does not \ + exist) */ \ + GAUGE(cache_size, Accumulate) /* Current certificates cached */ \ + GAUGE(pending_async_selections, Accumulate) /* Pending async selections count */ \ + HISTOGRAM(selection_latency, \ + Milliseconds) /* Certificate selection time in ms (only if not in cache) */ + +/** + * Struct definition for all Dynamic SDS Certificate Selector stats. + * @see stats_macros.h + */ +struct SelectorStats { + ALL_DYNAMIC_SDS_SELECTOR_STATS(GENERATE_COUNTER_STRUCT, GENERATE_GAUGE_STRUCT, + GENERATE_HISTOGRAM_STRUCT) +}; + +/** + * Generate the selector stats from a stats scope. + */ +inline SelectorStats generateSelectorStats(Stats::Scope& scope) { + return SelectorStats{ALL_DYNAMIC_SDS_SELECTOR_STATS(POOL_COUNTER(scope), POOL_GAUGE(scope), + POOL_HISTOGRAM(scope))}; +} + +} // namespace DynamicSds +} // namespace CertificateSelectors +} // namespace Tls +} // namespace TransportSockets +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/dynamic_sds_certificate_selector/test/BUILD b/contrib/dynamic_sds_certificate_selector/test/BUILD new file mode 100644 index 0000000000000..27f76e7783e82 --- /dev/null +++ b/contrib/dynamic_sds_certificate_selector/test/BUILD @@ -0,0 +1,24 @@ +load("//bazel:envoy_build_system.bzl", "envoy_cc_test", "envoy_contrib_package") + +licenses(["notice"]) # Apache 2 + +envoy_contrib_package() + +envoy_cc_test( + name = "dynamic_sds_certificate_selector_test", + srcs = ["dynamic_sds_certificate_selector_test.cc"], + deps = [ + "//contrib/dynamic_sds_certificate_selector/source:config", + "//source/common/config:utility_lib", + "//source/common/event:dispatcher_lib", + "//source/common/stats:isolated_store_lib", + "//test/common/tls:ssl_test_utils", + "//test/mocks/event:event_mocks", + "//test/mocks/init:init_mocks", + "//test/mocks/secret:secret_mocks", + "//test/mocks/server:factory_context_mocks", + "//test/mocks/ssl:ssl_mocks", + "//test/test_common:environment_lib", + "//test/test_common:utility_lib", + ], +) diff --git a/contrib/dynamic_sds_certificate_selector/test/dynamic_sds_certificate_selector_test.cc b/contrib/dynamic_sds_certificate_selector/test/dynamic_sds_certificate_selector_test.cc new file mode 100644 index 0000000000000..7a84fc1cfc7a5 --- /dev/null +++ b/contrib/dynamic_sds_certificate_selector/test/dynamic_sds_certificate_selector_test.cc @@ -0,0 +1,132 @@ +#include + +#include "source/common/stats/isolated_store_impl.h" + +#include "test/mocks/init/mocks.h" +#include "test/mocks/secret/mocks.h" +#include "test/mocks/server/server_factory_context.h" +#include "test/test_common/utility.h" + +#include "contrib/dynamic_sds_certificate_selector/source/dynamic_sds_certificate_selector.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::NiceMock; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace TransportSockets { +namespace Tls { +namespace CertificateSelectors { +namespace DynamicSds { +namespace { + +class DynamicSdsCertificateSelectorTest : public ::testing::Test { +public: + DynamicSdsCertificateSelectorTest() + : api_(Api::createApiForTest()), stats_scope_(stats_store_.createScope("test.")), + dispatcher_(api_->allocateDispatcher("test_thread")) {} + +protected: + void SetUp() override { + setupConfig(); + setupMocks(); + } + + void setupConfig() { + // Create a basic valid configuration + const std::string yaml_config = R"EOF( +sds_source: + api_config_source: + api_type: GRPC + transport_api_version: V3 + grpc_services: + - envoy_grpc: + cluster_name: sds_cluster +cache_config: + cache_ttl: 3600s + max_cache_entries: 1000 + eviction_interval: 300s +enable_default_selector_fallback: false +rules: +- sni_value_rewrite: + pattern: + google_re2: {} + regex: "^([^.]+)\\.example\\.com$" + substitution: "cert-\\1" +)EOF"; + + TestUtility::loadFromYaml(yaml_config, typed_config_); + } + + void setupMocks() { + ON_CALL(factory_context_, secretManager()).WillByDefault(ReturnRef(secret_manager_)); + ON_CALL(factory_context_, mainThreadDispatcher()).WillByDefault(ReturnRef(*dispatcher_)); + ON_CALL(factory_context_, scope()).WillByDefault(ReturnRef(*stats_scope_)); + } + + Api::ApiPtr api_; + Stats::IsolatedStoreImpl stats_store_; + Stats::ScopeSharedPtr stats_scope_; + Event::DispatcherPtr dispatcher_; + + DynamicSdsCertificateSelectorConfig typed_config_; + NiceMock secret_manager_; + NiceMock factory_context_; + // Create a simple mock context that provides getTlsContexts + class MockTlsCertificateSelectorContext : public Ssl::TlsCertificateSelectorContext { + public: + MOCK_METHOD(const std::vector&, getTlsContexts, (), (const)); + }; + NiceMock selector_ctx_; + NiceMock init_manager_; +}; + +TEST_F(DynamicSdsCertificateSelectorTest, ConfigFactoryCorrectName) { + DynamicSdsCertificateSelectorConfigFactory factory; + + EXPECT_EQ("envoy.tls.certificate_selectors.dynamic_sds", factory.name()); +} + +TEST_F(DynamicSdsCertificateSelectorTest, ValidConfigurationAccepted) { + DynamicSdsCertificateSelectorConfigFactory factory; + ProtobufMessage::NullValidationVisitorImpl validation_visitor; + absl::Status creation_status; + + auto selector_factory = factory.createTlsCertificateSelectorFactory( + typed_config_, factory_context_, validation_visitor, creation_status, false); + + EXPECT_TRUE(creation_status.ok()); + EXPECT_TRUE(selector_factory); +} + +TEST_F(DynamicSdsCertificateSelectorTest, InvalidConfigurationFallback) { + DynamicSdsCertificateSelectorConfigFactory factory; + ProtobufMessage::NullValidationVisitorImpl validation_visitor; + absl::Status creation_status; + + // Create invalid config with no SDS sources + DynamicSdsCertificateSelectorConfig invalid_config; + // No sds_sources will make it invalid due to proto validation + + auto selector_factory = factory.createTlsCertificateSelectorFactory( + invalid_config, factory_context_, validation_visitor, creation_status, false); + + EXPECT_TRUE(creation_status.ok()); + EXPECT_TRUE(selector_factory); + + auto default_selector_factory = + TlsCertificateSelectorConfigFactoryImpl::getDefaultTlsCertificateSelectorConfigFactory() + ->createTlsCertificateSelectorFactory(invalid_config, factory_context_, + validation_visitor, creation_status, false); + EXPECT_TRUE(typeid(selector_factory) == typeid(default_selector_factory)); +} + +} // namespace +} // namespace DynamicSds +} // namespace CertificateSelectors +} // namespace Tls +} // namespace TransportSockets +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/extensions_metadata.yaml b/contrib/extensions_metadata.yaml index ebef352ba1aa5..4f08c309131c7 100644 --- a/contrib/extensions_metadata.yaml +++ b/contrib/extensions_metadata.yaml @@ -83,6 +83,11 @@ envoy.tls.key_providers.qat: - envoy.tls.key_providers security_posture: robust_to_untrusted_downstream status: alpha +envoy.tls.certificate_selectors.dynamic_sds: + categories: + - envoy.tls.certificate_selectors + security_posture: robust_to_untrusted_downstream + status: wip envoy.bootstrap.vcl: categories: - envoy.bootstrap diff --git a/docs/root/api-v3/config/contrib/contrib.rst b/docs/root/api-v3/config/contrib/contrib.rst index 94661dfd98fc7..8c0433758a9ef 100644 --- a/docs/root/api-v3/config/contrib/contrib.rst +++ b/docs/root/api-v3/config/contrib/contrib.rst @@ -16,3 +16,4 @@ Contrib extensions qat/qat http_tcp_bridge/http_tcp_bridge tap_sinks/tap_sinks + tls/dynamic_sds_certificate_selector diff --git a/docs/root/api-v3/config/contrib/tls/dynamic_sds_certificate_selector.rst b/docs/root/api-v3/config/contrib/tls/dynamic_sds_certificate_selector.rst new file mode 100644 index 0000000000000..1196df09c1671 --- /dev/null +++ b/docs/root/api-v3/config/contrib/tls/dynamic_sds_certificate_selector.rst @@ -0,0 +1,8 @@ +Dynamic Sds Certificate Selector +====================== + +.. toctree:: + :glob: + :maxdepth: 2 + + ../../../extensions/transport_sockets/tls/certificate_selectors/dynamic_sds/v3alpha/* diff --git a/tools/extensions/extensions_schema.yaml b/tools/extensions/extensions_schema.yaml index 03e48628f01c8..598c4347c3bcb 100644 --- a/tools/extensions/extensions_schema.yaml +++ b/tools/extensions/extensions_schema.yaml @@ -136,6 +136,7 @@ categories: - envoy.path.match - envoy.path.rewrite - envoy.tls_handshakers +- envoy.tls.certificate_selectors - envoy.generic_proxy.filters - envoy.generic_proxy.codecs - envoy.load_balancing_policies