Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
],
)
Original file line number Diff line number Diff line change
@@ -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}}];
}
1 change: 1 addition & 0 deletions api/versioning/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions contrib/contrib_build_config.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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
#
Expand Down
48 changes: 48 additions & 0 deletions contrib/dynamic_sds_certificate_selector/source/BUILD
Original file line number Diff line number Diff line change
@@ -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",
],
)
122 changes: 122 additions & 0 deletions contrib/dynamic_sds_certificate_selector/source/certificate_cache.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#include "contrib/dynamic_sds_certificate_selector/source/certificate_cache.h"

#include <chrono>
#include <memory>

#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<ServerContextImpl>&& 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<CertificateEntry>(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
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#pragma once

#include <chrono>
#include <memory>
#include <string>

#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<ServerContextImpl> 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<MonotonicTime> last_access;

CertificateEntry(std::unique_ptr<ServerContextImpl> 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<CertificateEntry>;

/**
* Certificate cache for storing and managing dynamically fetched certificates.
*/
class CertificateCache : Logger::Loggable<Logger::Id::connection> {
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<ServerContextImpl>&& 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<std::string, std::unique_ptr<CertificateEntry>>
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
Loading