Skip to content

Commit 2b87f1d

Browse files
authored
spiffe validator: support multi-tenancy via filter state (#43886)
Commit Message: Add a condition to the trust domain selector using a per-connection filter state object. This is useful on multi-tenant set-ups, where a single validation context can be used between multiple tenants using the same proxy, as it happens to be on k8s (see the two trust domain labels in https://kube-agentic-networking.sigs.k8s.io/guides/quickstart/agent-identity-demo/#1-define-the-volume). This is only implemented on the xDS version of SPIFFE bundle since the upstream specification for the trust bundle does not cover this multi-tenant use yet. Risk Level: low (opt-in field) Testing: unit and integration Docs Changes: yes Release Notes: yes --------- Signed-off-by: Kuat Yessenov <kuat@google.com>
1 parent 2569536 commit 2b87f1d

16 files changed

Lines changed: 841 additions & 32 deletions

File tree

api/envoy/extensions/transport_sockets/tls/v3/tls_spiffe_validator_config.proto

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE;
4545
// - :ref:`allow_expired_certificate <envoy_v3_api_field_extensions.transport_sockets.tls.v3.CertificateValidationContext.allow_expired_certificate>` to allow expired certificates.
4646
// - :ref:`match_typed_subject_alt_names <envoy_v3_api_field_extensions.transport_sockets.tls.v3.CertificateValidationContext.match_typed_subject_alt_names>` to match **URI** SAN of certificates. Unlike the default validator, SPIFFE validator only matches **URI** SAN (which equals to SVID in SPIFFE terminology) and ignore other SAN types.
4747
//
48+
// To support multi-tenant use cases, a filter state object ``envoy.tls.cert_validator.spiffe.workload_trust_domain``
49+
// should be used to define the per-connection workload trust domain. When matching a peer trust domain, both the
50+
// workload and the peer trust domains are used in selecting the validation certificate. The filter state object
51+
// should be shared with the upstream to be used in the upstream TLS context SPIFFE validation context.
4852
message SPIFFECertValidatorConfig {
4953
message TrustDomain {
5054
// Name of the trust domain, ``example.com``, ``foo.bar.gov`` for example.
@@ -53,6 +57,11 @@ message SPIFFECertValidatorConfig {
5357

5458
// Specify a data source holding x.509 trust bundle used for validating incoming SVID(s) in this trust domain.
5559
config.core.v3.DataSource trust_bundle = 2;
60+
61+
// Optional workload trust domain selection condition. The filter object
62+
// ``envoy.tls.cert_validator.spiffe.workload_trust_domain`` must match exactly the value of this field.
63+
// If not specified, the filter state object must be absent or be empty to match this trust domain.
64+
string workload_trust_domain = 3;
5665
}
5766

5867
// This field specifies trust domains used for validating incoming X.509-SVID(s).

changelogs/current.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,10 @@ new_features:
267267
- area: filters
268268
change: |
269269
Added filters to update the filter state in :ref:`a listener filter <config_listener_filters_set_filter_state>`.
270+
- area: tls
271+
change: |
272+
Added a per-connection filter state object to select a workload trust domain in the SPIFFE validator in
273+
the multi-tenant deployments.
270274
- area: tls
271275
change: |
272276
Extended TLS certificate compression (RFC 8879): added brotli to QUIC (which already supported zlib),

docs/root/configuration/advanced/well_known_filter_state.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ The following lists the filter state object keys used by the Envoy extensions to
117117
Allows overriding the certificate to use per-connection using the :ref:`filter state certificate mapper
118118
<envoy_v3_api_msg_extensions.transport_sockets.tls.cert_mappers.filter_state_override.v3.Config>`.
119119

120+
``envoy.tls.cert_validator.spiffe.workload_trust_domain``
121+
Specifies per-connection workload trust domain to be used in the :ref:`SPIFFE certificate validator
122+
<envoy_v3_api_msg_extensions.transport_sockets.tls.v3.SPIFFECertValidatorConfig>`.
123+
120124
Filter state object factories
121125
-----------------------------
122126

source/extensions/transport_sockets/tls/cert_validator/spiffe/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ envoy_cc_extension(
1818
],
1919
external_deps = ["ssl"],
2020
deps = [
21+
"//envoy/router:string_accessor_interface",
2122
"//envoy/ssl:context_config_interface",
2223
"//envoy/ssl:ssl_socket_extended_info_interface",
2324
"//source/common/common:assert_lib",

source/extensions/transport_sockets/tls/cert_validator/spiffe/spiffe_validator.cc

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include "envoy/extensions/transport_sockets/tls/v3/tls_spiffe_validator_config.pb.h"
1212
#include "envoy/network/transport_socket.h"
1313
#include "envoy/registry/registry.h"
14+
#include "envoy/router/string_accessor.h"
1415
#include "envoy/ssl/context_config.h"
1516
#include "envoy/ssl/ssl_socket_extended_info.h"
1617

@@ -66,7 +67,7 @@ parseTrustBundles(absl::string_view trust_bundle_mapping_str) {
6667
// TODO: Duplicates are currently ignored and only the last value is used.
6768
// This is because our json parser auto de-dupes keys in the dict and
6869
// only include the last one in this iteration function.
69-
spiffe_data->trust_bundle_stores_[domain_name] = X509StorePtr(X509_STORE_new());
70+
spiffe_data->trust_bundle_stores_[domain_name][""] = X509StorePtr(X509_STORE_new());
7071

7172
ENVOY_LOG_TO_LOGGER(Logger::Registry::getLog(Logger::Id::secret), info,
7273
"Loading domain '{}' from SPIFFE bundle map", domain_name);
@@ -113,7 +114,7 @@ parseTrustBundles(absl::string_view trust_bundle_mapping_str) {
113114
fmt::format("Invalid x509 object in certs for domain '{}'", domain_name));
114115
return false;
115116
}
116-
if (X509_STORE_add_cert(spiffe_data->trust_bundle_stores_[domain_name].get(),
117+
if (X509_STORE_add_cert(spiffe_data->trust_bundle_stores_[domain_name][""].get(),
117118
x509.get()) != 1) {
118119
parsing_status = absl::InternalError(
119120
fmt::format("Failed to add x509 object while loading certs for domain '{}'",
@@ -186,11 +187,15 @@ SPIFFEValidator::SPIFFEValidator(const Envoy::Ssl::CertificateValidationContextC
186187
spiffe_data_ = std::make_shared<SpiffeData>();
187188
spiffe_data_->trust_bundle_stores_.reserve(message.trust_domains().size());
188189
for (auto& domain : message.trust_domains()) {
189-
if (spiffe_data_->trust_bundle_stores_.find(domain.name()) !=
190-
spiffe_data_->trust_bundle_stores_.end()) {
191-
creation_status = absl::InvalidArgumentError(absl::StrCat(
192-
"Multiple trust bundles are given for one trust domain for ", domain.name()));
193-
return;
190+
if (auto it = spiffe_data_->trust_bundle_stores_.find(domain.name());
191+
it != spiffe_data_->trust_bundle_stores_.end()) {
192+
if (auto local_it = it->second.find(domain.workload_trust_domain());
193+
local_it != it->second.end()) {
194+
creation_status = absl::InvalidArgumentError(
195+
absl::StrCat("Multiple trust bundles are given for one trust domain for ",
196+
domain.name(), ", workload: ", domain.workload_trust_domain()));
197+
return;
198+
}
194199
}
195200

196201
auto cert = Config::DataSource::read(domain.trust_bundle(), true, config->api());
@@ -233,7 +238,8 @@ SPIFFEValidator::SPIFFEValidator(const Envoy::Ssl::CertificateValidationContextC
233238
if (has_crl) {
234239
X509_STORE_set_flags(store.get(), X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL);
235240
}
236-
spiffe_data_->trust_bundle_stores_[domain.name()] = std::move(store);
241+
spiffe_data_->trust_bundle_stores_[domain.name()][domain.workload_trust_domain()] =
242+
std::move(store);
237243
}
238244

239245
initializeCertExpirationStats(scope, config->caCertName());
@@ -286,14 +292,15 @@ absl::StatusOr<int> SPIFFEValidator::initializeSslContexts(std::vector<SSL_CTX*>
286292
bool SPIFFEValidator::verifyCertChainUsingTrustBundleStore(X509& leaf_cert,
287293
STACK_OF(X509)* cert_chain,
288294
X509_VERIFY_PARAM* verify_param,
295+
absl::string_view workload_trust_domain,
289296
std::string& error_details) {
290297
if (!SPIFFEValidator::certificatePrecheck(&leaf_cert)) {
291298
error_details = "verify cert failed: cert precheck";
292299
stats_.fail_verify_error_.inc();
293300
return false;
294301
}
295302

296-
auto trust_bundle = getTrustBundleStore(&leaf_cert);
303+
auto trust_bundle = getTrustBundleStore(&leaf_cert, workload_trust_domain);
297304
if (!trust_bundle) {
298305
error_details = "verify cert failed: no trust bundle store";
299306
stats_.fail_verify_error_.inc();
@@ -328,21 +335,41 @@ bool SPIFFEValidator::verifyCertChainUsingTrustBundleStore(X509& leaf_cert,
328335
return san_match;
329336
}
330337

338+
constexpr absl::string_view WorkloadTrustDomainKey =
339+
"envoy.tls.cert_validator.spiffe.workload_trust_domain";
340+
331341
ValidationResults SPIFFEValidator::doVerifyCertChain(
332342
STACK_OF(X509)& cert_chain, Ssl::ValidateResultCallbackPtr /*callback*/,
333-
const Network::TransportSocketOptionsConstSharedPtr& /*transport_socket_options*/,
334-
SSL_CTX& ssl_ctx, const CertValidator::ExtraValidationContext& /*validation_context*/,
335-
bool /*is_server*/, absl::string_view /*host_name*/) {
343+
const Network::TransportSocketOptionsConstSharedPtr& transport_socket_options, SSL_CTX& ssl_ctx,
344+
const CertValidator::ExtraValidationContext& validation_context, bool is_server,
345+
absl::string_view /*host_name*/) {
336346
if (sk_X509_num(&cert_chain) == 0) {
337347
stats_.fail_verify_error_.inc();
338348
return {ValidationResults::ValidationStatus::Failed,
339349
Envoy::Ssl::ClientValidationStatus::NotValidated, absl::nullopt,
340350
"verify cert failed: empty cert chain"};
341351
}
342352
X509* leaf_cert = sk_X509_value(&cert_chain, 0);
353+
const Router::StringAccessor* obj = nullptr;
354+
if (is_server) {
355+
if (auto* cb = validation_context.callbacks; cb) {
356+
const StreamInfo::StreamInfo& info = cb->connection().streamInfo();
357+
obj = info.filterState().getDataReadOnly<Router::StringAccessor>(WorkloadTrustDomainKey);
358+
}
359+
} else {
360+
if (transport_socket_options) {
361+
for (const auto& obj_meta : transport_socket_options->downstreamSharedFilterStateObjects()) {
362+
if (obj_meta.name_ == WorkloadTrustDomainKey) {
363+
obj = dynamic_cast<const Router::StringAccessor*>(obj_meta.data_.get());
364+
break;
365+
}
366+
}
367+
}
368+
}
369+
absl::string_view workload_trust_domain = obj ? obj->asString() : "";
343370
std::string error_details;
344-
bool verified = verifyCertChainUsingTrustBundleStore(*leaf_cert, &cert_chain,
345-
SSL_CTX_get0_param(&ssl_ctx), error_details);
371+
bool verified = verifyCertChainUsingTrustBundleStore(
372+
*leaf_cert, &cert_chain, SSL_CTX_get0_param(&ssl_ctx), workload_trust_domain, error_details);
346373
return verified ? ValidationResults{ValidationResults::ValidationStatus::Successful,
347374
Envoy::Ssl::ClientValidationStatus::Validated, absl::nullopt,
348375
absl::nullopt}
@@ -351,7 +378,8 @@ ValidationResults SPIFFEValidator::doVerifyCertChain(
351378
error_details};
352379
}
353380

354-
X509_STORE* SPIFFEValidator::getTrustBundleStore(X509* leaf_cert) {
381+
X509_STORE* SPIFFEValidator::getTrustBundleStore(X509* leaf_cert,
382+
absl::string_view workload_trust_domain) {
355383
bssl::UniquePtr<GENERAL_NAMES> san_names(static_cast<GENERAL_NAMES*>(
356384
X509_get_ext_d2i(leaf_cert, NID_subject_alt_name, nullptr, nullptr)));
357385
if (!san_names) {
@@ -376,8 +404,14 @@ X509_STORE* SPIFFEValidator::getTrustBundleStore(X509* leaf_cert) {
376404

377405
auto spiffe_data = getSpiffeData();
378406
auto target_store = spiffe_data->trust_bundle_stores_.find(trust_domain);
379-
return target_store != spiffe_data->trust_bundle_stores_.end() ? target_store->second.get()
380-
: nullptr;
407+
if (target_store == spiffe_data->trust_bundle_stores_.end()) {
408+
return nullptr;
409+
}
410+
auto it = target_store->second.find(workload_trust_domain);
411+
if (it == target_store->second.end()) {
412+
return nullptr;
413+
}
414+
return it->second.get();
381415
}
382416

383417
bool SPIFFEValidator::certificatePrecheck(X509* leaf_cert) {

source/extensions/transport_sockets/tls/cert_validator/spiffe/spiffe_validator.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ namespace Tls {
3434
using X509StorePtr = CSmartPtr<X509_STORE, X509_STORE_free>;
3535

3636
struct SpiffeData {
37-
absl::flat_hash_map<std::string, CSmartPtr<X509_STORE, X509_STORE_free>> trust_bundle_stores_;
37+
// Mapping for "peer trust domain" -> "local trust domain" -> certificate.
38+
absl::flat_hash_map<std::string,
39+
absl::flat_hash_map<std::string, CSmartPtr<X509_STORE, X509_STORE_free>>>
40+
trust_bundle_stores_;
3841
std::vector<bssl::UniquePtr<X509>> ca_certs_;
3942
};
4043

@@ -70,7 +73,7 @@ class SPIFFEValidator : public CertValidator, Logger::Loggable<Logger::Id::secre
7073
Envoy::Ssl::CertificateDetailsPtr getCaCertInformation() const override;
7174

7275
// Utility functions
73-
X509_STORE* getTrustBundleStore(X509* leaf_cert);
76+
X509_STORE* getTrustBundleStore(X509* leaf_cert, absl::string_view workload_trust_domain);
7477
static std::string extractTrustDomain(const std::string& san);
7578
static bool certificatePrecheck(X509* leaf_cert);
7679
OptRef<SpiffeData> getSpiffeData() const {
@@ -84,6 +87,7 @@ class SPIFFEValidator : public CertValidator, Logger::Loggable<Logger::Id::secre
8487
private:
8588
bool verifyCertChainUsingTrustBundleStore(X509& leaf_cert, STACK_OF(X509)* cert_chain,
8689
X509_VERIFY_PARAM* verify_param,
90+
absl::string_view workload_trust_domain,
8791
std::string& error_details);
8892

8993
void initializeCertExpirationStats(Stats::Scope& scope, const std::string& cert_name);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-----BEGIN CERTIFICATE REQUEST-----
2+
MIIDCTCCAfECAQAwdjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWEx
3+
FjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNVBAoMBEx5ZnQxGTAXBgNVBAsM
4+
EEx5ZnQgRW5naW5lZXJpbmcxEDAOBgNVBAMMB1Rlc3QgQ0EwggEiMA0GCSqGSIb3
5+
DQEBAQUAA4IBDwAwggEKAoIBAQDbOPJ7bf3WVPm7IuCvwtAGmssZe57ztyjOg1o8
6+
JVGnmxhrk4T+8Jtq0yg/Qma4Aipy0hlYIgsk5NtBD6YhVlAAXqh0MBt7ppKl2+Qe
7+
AEEJ9qT+59S+YdHfJjcwGKGXYsgzvlkGjc81s1cRyA8kUxtxgEeDh7RFf211ipBe
8+
lhaBMRb9T+x2p/roXvK5l0iUVd7CSZ9WZlqwMj/JHxtyKxHKPaZu/OBo0ieKnNUj
9+
E4vbJ6SehY1MPZl33MxaCNVgUXSstIDOawwZp/Wkzni/fHh8CDucuYiLHpmR6VFF
10+
D70OYiLs1qZ27DvNSyE3IjB3WP4hcCzEHyKe4Y3qRsEepad1AgMBAAGgTjBMBgkq
11+
hkiG9w0BCQ4xPzA9MAwGA1UdEwQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1Ud
12+
DgQWBBSdYEZKo5ykKAgjW/MmjXsdvillLzANBgkqhkiG9w0BAQsFAAOCAQEAyehS
13+
aCwwc+rICPVzRI3VYJcls81qTBfQoHxJn3CLhwFeTYYcbGHhh9+JZh/NY/qkDvW0
14+
9ghnvkga5MUemuqpqXc38Vm6AIESdvA1OGDtViuTdRrcD44Bak6QzTvMgPGC1dhr
15+
+c6ywTOQ7Q4ZYs63Tvc2MJkw9Y/zpcisgmYT4VD5ocyg2ENDg6DXmIwCm9IO0FUK
16+
/Thve+Ro2wI2JRZ4FUfN7DghT6azsx0VW+7hd1yBvV5/s1smEF47uQmoBM+RnXTR
17+
94sCc4ROHfD1Mqwnub9pjsLHN7xijStb9kGc8G0ys0qPv66YrjzNHFzvK7os8m+q
18+
yJ0GSc+qanlQuKM8gQ==
19+
-----END CERTIFICATE REQUEST-----

0 commit comments

Comments
 (0)