diff --git a/api/envoy/extensions/filters/http/header_to_metadata/v3/header_to_metadata.proto b/api/envoy/extensions/filters/http/header_to_metadata/v3/header_to_metadata.proto index 5e399790a7eca..e3caaeda6d48b 100644 --- a/api/envoy/extensions/filters/http/header_to_metadata/v3/header_to_metadata.proto +++ b/api/envoy/extensions/filters/http/header_to_metadata/v3/header_to_metadata.proto @@ -86,7 +86,7 @@ message Config { } // A Rule defines what metadata to apply when a header is present or missing. - // [#next-free-field: 6] + // [#next-free-field: 7] message Rule { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.header_to_metadata.v2.Config.Rule"; @@ -122,6 +122,10 @@ message Config { // This prevents headers from leaking. // This field is not supported in case of a cookie. bool remove = 4; + + // If this is set, the applied metadata value will be used as one of the SANs to validate the + // certificate in the upstream connection. This is used to override the SAN in the TLS context. + bool use_as_upstream_subject_alt_name = 6; } // The list of rules to apply to requests. diff --git a/source/extensions/filters/http/header_to_metadata/BUILD b/source/extensions/filters/http/header_to_metadata/BUILD index ff6b8466a427e..b93ae4ca944a4 100644 --- a/source/extensions/filters/http/header_to_metadata/BUILD +++ b/source/extensions/filters/http/header_to_metadata/BUILD @@ -22,6 +22,7 @@ envoy_cc_library( "//source/common/config:well_known_names", "//source/common/http:header_utility_lib", "//source/common/http:utility_lib", + "//source/common/network:upstream_subject_alt_names_lib", "@envoy_api//envoy/extensions/filters/http/header_to_metadata/v3:pkg_cc_proto", ], ) diff --git a/source/extensions/filters/http/header_to_metadata/header_to_metadata_filter.cc b/source/extensions/filters/http/header_to_metadata/header_to_metadata_filter.cc index 2b556e40a45f2..c435facaad9cc 100644 --- a/source/extensions/filters/http/header_to_metadata/header_to_metadata_filter.cc +++ b/source/extensions/filters/http/header_to_metadata/header_to_metadata_filter.cc @@ -7,6 +7,7 @@ #include "source/common/config/well_known_names.h" #include "source/common/http/header_utility.h" #include "source/common/http/utility.h" +#include "source/common/network/upstream_subject_alt_names.h" #include "source/common/protobuf/protobuf.h" #include "absl/strings/numbers.h" @@ -216,8 +217,8 @@ const std::string& HeaderToMetadataFilter::decideNamespace(const std::string& ns } // add metadata['key']= value depending on header present or missing case -void HeaderToMetadataFilter::applyKeyValue(std::string&& value, const Rule& rule, - const KeyValuePair& keyval, StructMap& np) { +std::string HeaderToMetadataFilter::applyKeyValue(std::string&& value, const Rule& rule, + const KeyValuePair& keyval, StructMap& np) { if (!keyval.value().empty()) { value = keyval.value(); } else { @@ -232,31 +233,46 @@ void HeaderToMetadataFilter::applyKeyValue(std::string&& value, const Rule& rule } else { ENVOY_LOG(debug, "value is empty, not adding metadata"); } + + return std::move(value); } void HeaderToMetadataFilter::writeHeaderToMetadata(Http::HeaderMap& headers, const HeaderToMetadataRules& rules, Http::StreamFilterCallbacks& callbacks) { StructMap structs_by_namespace; + std::vector upstream_subject_alt_names{}; for (const auto& rule : rules) { const auto& proto_rule = rule.rule(); absl::optional value = rule.selector_->extract(headers); if (value && proto_rule.has_on_header_present()) { - applyKeyValue(std::move(value).value_or(""), rule, proto_rule.on_header_present(), - structs_by_namespace); + auto applied_value = applyKeyValue(std::move(value).value_or(""), rule, + proto_rule.on_header_present(), structs_by_namespace); + if (!applied_value.empty() && proto_rule.use_as_upstream_subject_alt_name()) { + upstream_subject_alt_names.push_back(std::move(applied_value)); + } } else if (!value && proto_rule.has_on_header_missing()) { applyKeyValue(std::move(value).value_or(""), rule, proto_rule.on_header_missing(), structs_by_namespace); } } + + auto& stream_info = callbacks.streamInfo(); + // Any matching rules? if (!structs_by_namespace.empty()) { for (auto const& entry : structs_by_namespace) { - callbacks.streamInfo().setDynamicMetadata(entry.first, entry.second); + stream_info.setDynamicMetadata(entry.first, entry.second); } } + + if (!upstream_subject_alt_names.empty()) { + auto data = std::make_unique(upstream_subject_alt_names); + stream_info.filterState()->setData(Network::UpstreamSubjectAltNames::key(), std::move(data), + StreamInfo::FilterState::StateType::Mutable); + } } // TODO(rgs1): this belongs in one of the filter interfaces, see issue #10164. diff --git a/source/extensions/filters/http/header_to_metadata/header_to_metadata_filter.h b/source/extensions/filters/http/header_to_metadata/header_to_metadata_filter.h index b135f412bca81..3b383a6616aff 100644 --- a/source/extensions/filters/http/header_to_metadata/header_to_metadata_filter.h +++ b/source/extensions/filters/http/header_to_metadata/header_to_metadata_filter.h @@ -190,7 +190,7 @@ class HeaderToMetadataFilter : public Http::StreamFilter, Http::StreamFilterCallbacks& callbacks); bool addMetadata(StructMap&, const std::string&, const std::string&, std::string, ValueType, ValueEncode) const; - void applyKeyValue(std::string&&, const Rule&, const KeyValuePair&, StructMap&); + std::string applyKeyValue(std::string&&, const Rule&, const KeyValuePair&, StructMap&); const std::string& decideNamespace(const std::string& nspace) const; const Config* getConfig() const; }; diff --git a/test/extensions/filters/http/header_to_metadata/header_to_metadata_filter_test.cc b/test/extensions/filters/http/header_to_metadata/header_to_metadata_filter_test.cc index 02c5d56f6e8ec..59f6aa7226a96 100644 --- a/test/extensions/filters/http/header_to_metadata/header_to_metadata_filter_test.cc +++ b/test/extensions/filters/http/header_to_metadata/header_to_metadata_filter_test.cc @@ -4,6 +4,7 @@ #include "source/common/common/base64.h" #include "source/common/http/header_map_impl.h" +#include "source/common/network/upstream_subject_alt_names.h" #include "source/common/protobuf/protobuf.h" #include "source/extensions/filters/http/header_to_metadata/header_to_metadata_filter.h" @@ -587,6 +588,65 @@ TEST_F(HeaderToMetadataTest, RegexSubstitution) { } } +/** + * Regex substitution and use as upstream SAN. + */ +TEST_F(HeaderToMetadataTest, RegexSubstitutionUpstreamSAN) { + const std::string config = R"EOF( +request_rules: + - header: :path + use_as_upstream_subject_alt_name: true + on_header_present: + metadata_namespace: envoy.lb + key: cluster + regex_value_rewrite: + pattern: + google_re2: {} + regex: "^/(cluster[\\d\\w-]+)/?.*$" + substitution: "\\1-san" +)EOF"; + initializeFilter(config); + + Http::TestRequestHeaderMapImpl headers{{":path", "/cluster-prod-001/x/y"}}; + std::map expected = {{"cluster", "cluster-prod-001-san"}}; + + EXPECT_CALL(decoder_callbacks_, streamInfo()).WillRepeatedly(ReturnRef(req_info_)); + EXPECT_CALL(req_info_, setDynamicMetadata("envoy.lb", MapEq(expected))); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); + EXPECT_EQ("cluster-prod-001-san", req_info_.filterState() + ->getDataReadOnly( + Network::UpstreamSubjectAltNames::key()) + .value()[0]); +} + +/** + * Regex substitution and use as upstream SAN: no match. + */ +TEST_F(HeaderToMetadataTest, RegexSubstitutionUpstreamSANNoMatch) { + const std::string config = R"EOF( +request_rules: + - header: x-use-as-upstream-san + use_as_upstream_subject_alt_name: true + on_header_present: + metadata_namespace: envoy.lb + key: cluster + regex_value_rewrite: + pattern: + google_re2: {} + regex: "^/(cluster[\\d\\w-]+)/?.*$" + substitution: "\\1-san" +)EOF"; + initializeFilter(config); + + Http::TestRequestHeaderMapImpl headers{{":path", "/foo"}}; + + EXPECT_CALL(decoder_callbacks_, streamInfo()).WillRepeatedly(ReturnRef(req_info_)); + EXPECT_CALL(req_info_, setDynamicMetadata(_, _)).Times(0); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); + EXPECT_FALSE(req_info_.filterState()->hasData( + Network::UpstreamSubjectAltNames::key())); +} + /** * Missing case is not executed when header is present. */