Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE;
// [#extension: envoy.filters.http.aws_request_signing]

// Top level configuration for the AWS request signing filter.
// [#next-free-field: 9]
// [#next-free-field: 10]
message AwsRequestSigning {
option (udpa.annotations.versioning).previous_message_type =
"envoy.config.filter.http.aws_request_signing.v2alpha.AwsRequestSigning";
Expand Down Expand Up @@ -58,14 +58,14 @@ message AwsRequestSigning {
// When signing_algorithm is set to ``AWS_SIGV4`` the region is a standard AWS `region <https://docs.aws.amazon.com/general/latest/gr/rande.html>`_ string for the service
// hosting the HTTP endpoint.
//
// Example: us-west-2
// Example: ``us-west-2``
//
// When signing_algorithm is set to ``AWS_SIGV4A`` the region is used as a region set.
//
// A region set is a comma separated list of AWS regions, such as ``us-east-1,us-east-2`` or wildcard ``*``
// or even region strings containing wildcards such as ``us-east-*``
//
// Example: '*'
// Example: ``'*'``
//
// By configuring a region set, a SigV4A signed request can be sent to multiple regions, rather than being
// valid for only a single region destination.
Expand All @@ -91,11 +91,15 @@ message AwsRequestSigning {
// any patterns defined in the StringMatcher proto (e.g. exact string, prefix, regex, etc).
//
// Example:
// match_excluded_headers:
// - prefix: x-envoy
// - exact: foo
// - exact: bar
// When applied, all headers that start with "x-envoy" and headers "foo" and "bar" will not be signed.
//
// .. code-block:: yaml
//
// match_excluded_headers:
// - prefix: x-envoy
// - exact: foo
// - exact: bar
//
// When applied, all headers that start with ``x-envoy`` and headers ``foo`` and ``bar`` will not be signed.
repeated type.matcher.v3.StringMatcher match_excluded_headers = 5;

// Optional Signing algorithm specifier, either ``AWS_SIGV4`` or ``AWS_SIGV4A``, defaulting to ``AWS_SIGV4``.
Expand All @@ -112,6 +116,23 @@ message AwsRequestSigning {
// The credential provider for signing the request. This is optional and if not set,
// it will be retrieved using the procedure described in :ref:`config_http_filters_aws_request_signing`.
common.aws.v3.AwsCredentialProvider credential_provider = 8;

// A list of request header string matchers that will be included during signing. The included header can be matched by
// any patterns defined in the StringMatcher proto (e.g. exact string, prefix, regex, etc).
// match_included_headers takes precedence over match_excluded_headers - if match_included_headers is set, only those headers will be signed and match_excluded_headers will be ignored.
// Required headers for signing such as ``host`` will always be signed regardless of this setting. The required headers are determined via ``CanonicalHeaders`` section in the AWS documentation `here <https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html#create-canonical-request>`_.
//
// Example:
//
// .. code-block:: yaml
//
// match_included_headers:
// - prefix: x-envoy
// - exact: foo
// - exact: bar
//
// When applied, all headers that start with ``x-envoy`` and headers ``foo`` and ``bar`` will be signed and all other headers will be excluded from signing except required headers.
repeated type.matcher.v3.StringMatcher match_included_headers = 9;
}

message AwsRequestSigningPerRoute {
Expand Down
4 changes: 4 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -120,5 +120,9 @@ new_features:
Add a new quic configuration field, 'max_sessions_per_event_loop', to QuicProtocolOptions in Envoy listener.
This allows tuning the maximum number of new QUIC sessions created within a single event loop.
The default value is 16, preserving the previous hardcoded limit.
- area: aws
change: |
Added new feature ``match_included_headers`` to the request signing extension, that allows for a positive header match and
excludes all other non-SigV4-required headers.

deprecated:
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,8 @@ CredentialsProviderSharedPtr CommonCredentialsProviderChain::createAssumeRoleCre
// Create our own signer specifically for signing AssumeRole API call
auto signer = std::make_unique<SigV4SignerImpl>(
STS_SERVICE_NAME, region, credentials_provider_chain, context,
Extensions::Common::Aws::AwsSigningHeaderExclusionVector{});
Extensions::Common::Aws::AwsSigningHeaderMatcherVector{},
Extensions::Common::Aws::AwsSigningHeaderMatcherVector{});

auto credential_provider = std::make_shared<AssumeRoleCredentialsProvider>(
context, aws_cluster_manager, cluster_name, MetadataFetcher::create, region, refresh_state,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ absl::Status IAMRolesAnywhereSignerBaseImpl::sign(Http::RequestHeaderMap& header
addRequiredCertHeaders(headers, x509_credentials);

const auto canonical_headers =
Utility::canonicalizeHeaders(headers, std::vector<Matchers::StringMatcherPtr>{});
Utility::canonicalizeHeaders(headers, std::vector<Matchers::StringMatcherPtr>{},
std::vector<Matchers::StringMatcherPtr>{});

// Phase 1: Create a canonical request
const auto credential_scope = createCredentialScope(short_date, override_region);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class IAMRolesAnywhereSignatureConstants {
static constexpr uint16_t DefaultExpiration = 900;
};

using AwsSigningHeaderExclusionVector = std::vector<envoy::type::matcher::v3::StringMatcher>;
using AwsSigningHeaderMatcherVector = std::vector<envoy::type::matcher::v3::StringMatcher>;

/**
* Implementation of the Signature V4 signing process using X509 certificates.
Expand Down
3 changes: 2 additions & 1 deletion source/extensions/common/aws/signer_base_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ absl::Status SignerBaseImpl::sign(Http::RequestHeaderMap& headers, const std::st
addRequiredHeaders(headers, long_date, credentials.sessionToken(), override_region);
}

const auto canonical_headers = Utility::canonicalizeHeaders(headers, excluded_header_matchers_);
const auto canonical_headers =
Utility::canonicalizeHeaders(headers, excluded_header_matchers_, included_header_matchers_);

// Phase 1: Create a canonical request
const auto credential_scope = createCredentialScope(short_date, override_region);
Expand Down
12 changes: 9 additions & 3 deletions source/extensions/common/aws/signer_base_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class SignatureConstants {
static constexpr absl::string_view AuthorizationCredentialFormat = "{}/{}";
};

using AwsSigningHeaderExclusionVector = std::vector<envoy::type::matcher::v3::StringMatcher>;
using AwsSigningHeaderMatcherVector = std::vector<envoy::type::matcher::v3::StringMatcher>;

/**
* Implementation of the Signature V4 signing process.
Expand All @@ -63,7 +63,8 @@ class SignerBaseImpl : public Signer, public Logger::Loggable<Logger::Id::aws> {
SignerBaseImpl(absl::string_view service_name, absl::string_view region,
const CredentialsProviderChainSharedPtr& credentials_provider_chain,
Server::Configuration::CommonFactoryContext& context,
const AwsSigningHeaderExclusionVector& matcher_config,
const AwsSigningHeaderMatcherVector& exclude_matcher_config,
const AwsSigningHeaderMatcherVector& include_matcher_config,
const bool query_string = false,
const uint16_t expiration_time = SignatureQueryParameterValues::DefaultExpiration)
: service_name_(service_name), region_(region),
Expand All @@ -72,10 +73,14 @@ class SignerBaseImpl : public Signer, public Logger::Loggable<Logger::Id::aws> {
expiration_time_(expiration_time), time_source_(context.timeSource()),
long_date_formatter_(std::string(SignatureConstants::LongDateFormat)),
short_date_formatter_(std::string(SignatureConstants::ShortDateFormat)) {
for (const auto& matcher : matcher_config) {
for (const auto& matcher : exclude_matcher_config) {
excluded_header_matchers_.emplace_back(
std::make_unique<Matchers::StringMatcherImpl>(matcher, context));
}
for (const auto& matcher : include_matcher_config) {
included_header_matchers_.emplace_back(
std::make_unique<Matchers::StringMatcherImpl>(matcher, context));
}
}

absl::Status sign(Http::RequestMessage& message, bool sign_body = false,
Expand Down Expand Up @@ -152,6 +157,7 @@ class SignerBaseImpl : public Signer, public Logger::Loggable<Logger::Id::aws> {
Http::Headers::get().ForwardedFor.get(), Http::Headers::get().ForwardedProto.get(),
"x-amzn-trace-id"};
std::vector<Matchers::StringMatcherPtr> excluded_header_matchers_;
std::vector<Matchers::StringMatcherPtr> included_header_matchers_;
CredentialsProviderChainSharedPtr credentials_provider_chain_;
const bool query_string_;
const uint16_t expiration_time_;
Expand Down
9 changes: 5 additions & 4 deletions source/extensions/common/aws/signers/sigv4_signer_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class SigV4SignatureConstants : public SignatureConstants {
static constexpr absl::string_view SigV4Algorithm{"AWS4-HMAC-SHA256"};
};

using AwsSigningHeaderExclusionVector = std::vector<envoy::type::matcher::v3::StringMatcher>;
using AwsSigningHeaderMatcherVector = std::vector<envoy::type::matcher::v3::StringMatcher>;

/**
* Implementation of the Signature V4 signing process.
Expand All @@ -39,11 +39,12 @@ class SigV4SignerImpl : public SignerBaseImpl {
SigV4SignerImpl(absl::string_view service_name, absl::string_view region,
const CredentialsProviderChainSharedPtr& credentials_provider,
Server::Configuration::CommonFactoryContext& context,
const AwsSigningHeaderExclusionVector& matcher_config,
const AwsSigningHeaderMatcherVector& exclude_matcher_config,
const AwsSigningHeaderMatcherVector& include_matcher_config,
const bool query_string = false,
const uint16_t expiration_time = SignatureQueryParameterValues::DefaultExpiration)
: SignerBaseImpl(service_name, region, credentials_provider, context, matcher_config,
query_string, expiration_time) {}
: SignerBaseImpl(service_name, region, credentials_provider, context, exclude_matcher_config,
include_matcher_config, query_string, expiration_time) {}

private:
std::string createCredentialScope(const absl::string_view short_date,
Expand Down
2 changes: 1 addition & 1 deletion source/extensions/common/aws/signers/sigv4a_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#include "source/common/singleton/const_singleton.h"
#include "source/extensions/common/aws/signer_base_impl.h"

using AwsSigningHeaderExclusionVector = std::vector<envoy::type::matcher::v3::StringMatcher>;
using AwsSigningHeaderMatcherVector = std::vector<envoy::type::matcher::v3::StringMatcher>;

namespace Envoy {
namespace Extensions {
Expand Down
7 changes: 4 additions & 3 deletions source/extensions/common/aws/signers/sigv4a_signer_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,13 @@ class SigV4ASignerImpl : public SignerBaseImpl {
absl::string_view service_name, absl::string_view region,
const CredentialsProviderChainSharedPtr& credentials_provider,
Server::Configuration::CommonFactoryContext& context,
const AwsSigningHeaderExclusionVector& matcher_config, const bool query_string = false,
const AwsSigningHeaderMatcherVector& exclude_matcher_config,
const AwsSigningHeaderMatcherVector& include_matcher_config, const bool query_string = false,
const uint16_t expiration_time = SignatureQueryParameterValues::DefaultExpiration,
std::unique_ptr<SigV4AKeyDerivationBase> key_derivation_ptr =
std::make_unique<SigV4AKeyDerivation>())
: SignerBaseImpl(service_name, region, credentials_provider, context, matcher_config,
query_string, expiration_time),
: SignerBaseImpl(service_name, region, credentials_provider, context, exclude_matcher_config,
include_matcher_config, query_string, expiration_time),
key_derivation_ptr_(std::move(key_derivation_ptr)) {}

private:
Expand Down
68 changes: 46 additions & 22 deletions source/extensions/common/aws/utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -28,38 +28,62 @@ constexpr char DEFAULT_AWS_CONFIG_FILE[] = "/.aws/config";

std::map<std::string, std::string>
Utility::canonicalizeHeaders(const Http::RequestHeaderMap& headers,
const std::vector<Matchers::StringMatcherPtr>& excluded_headers) {
const std::vector<Matchers::StringMatcherPtr>& excluded_headers,
const std::vector<Matchers::StringMatcherPtr>& included_headers) {
std::map<std::string, std::string> out;
headers.iterate(
[&out, &excluded_headers](const Http::HeaderEntry& entry) -> Http::HeaderMap::Iterate {
// Skip empty headers
if (entry.key().empty() || entry.value().empty()) {
return Http::HeaderMap::Iterate::Continue;
}
// Pseudo-headers should not be canonicalized
if (!entry.key().getStringView().empty() && entry.key().getStringView()[0] == ':') {

headers.iterate([&out, &excluded_headers,
&included_headers](const Http::HeaderEntry& entry) -> Http::HeaderMap::Iterate {
// Skip empty headers
if (entry.key().empty() || entry.value().empty()) {
return Http::HeaderMap::Iterate::Continue;
}
const auto key = entry.key().getStringView();

// Pseudo-headers should not be canonicalized
if (!key.empty() && key[0] == ':') {
return Http::HeaderMap::Iterate::Continue;
}

// Always include x-amz-* and content-type headers per AWS signing requirements
const auto key_lower = absl::AsciiStrToLower(key);
const bool is_required_header =
absl::StartsWith(key_lower, "x-amz-") || key_lower == "content-type";

if (!is_required_header) {
if (!included_headers.empty()) {
// If included_headers is set, only include headers that match
if (!std::any_of(included_headers.begin(), included_headers.end(),
[&key](const Matchers::StringMatcherPtr& matcher) {
return matcher->match(key);
})) {
return Http::HeaderMap::Iterate::Continue;
}
const auto key = entry.key().getStringView();
} else {
// Otherwise use excluded_headers
if (std::any_of(excluded_headers.begin(), excluded_headers.end(),
[&key](const Matchers::StringMatcherPtr& matcher) {
return matcher->match(key);
})) {
return Http::HeaderMap::Iterate::Continue;
}
}
}

std::string value(entry.value().getStringView());
// Remove leading, trailing, and deduplicate repeated ascii spaces
absl::RemoveExtraAsciiWhitespace(&value);
const auto iter = out.find(std::string(entry.key().getStringView()));
// If the entry already exists, append the new value to the end
if (iter != out.end()) {
iter->second += fmt::format(",{}", value);
} else {
out.emplace(std::string(entry.key().getStringView()), value);
}
return Http::HeaderMap::Iterate::Continue;
});
std::string value(entry.value().getStringView());

// Remove leading, trailing, and deduplicate repeated ascii spaces
absl::RemoveExtraAsciiWhitespace(&value);
const std::string key_str(key);
const auto iter = out.find(key_str);
// If the entry already exists, append the new value to the end
if (iter != out.end()) {
iter->second += fmt::format(",{}", value);
} else {
out.emplace(std::move(key_str), std::move(value));
}
return Http::HeaderMap::Iterate::Continue;
});
// The AWS SDK has a quirk where it removes "default ports" (80, 443) from the host headers
// Additionally, we canonicalize the :authority header as "host"
// TODO(suniltheta): This may need to be tweaked to canonicalize :authority for HTTP/2 requests
Expand Down
6 changes: 5 additions & 1 deletion source/extensions/common/aws/utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,15 @@ class Utility : public Logger::Loggable<Logger::Id::aws> {
* See https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
* @param headers a header map to canonicalize.
* @param excluded_headers a list of string matchers to exclude a given header from signing.
* @param included_headers a list of string matchers to include a given header from signing.
* included_headers will take precedence over excluded_headers and if included_headers is
* non-empty, only headers that match included_headers will be signed.
* @return a std::map of canonicalized headers to be used in building a canonical request.
*/
static std::map<std::string, std::string>
canonicalizeHeaders(const Http::RequestHeaderMap& headers,
const std::vector<Matchers::StringMatcherPtr>& excluded_headers);
const std::vector<Matchers::StringMatcherPtr>& excluded_headers,
const std::vector<Matchers::StringMatcherPtr>& included_headers);

/**
* Creates an AWS Signature V4 canonical request string.
Expand Down
6 changes: 4 additions & 2 deletions source/extensions/filters/http/aws_lambda/config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ absl::StatusOr<Http::FilterFactoryCb> AwsLambdaFilterFactory::createFilterFactor
service_name, region, std::move(credentials_provider), server_context,
// TODO: extend API to allow specifying header exclusion. ref:
// https://github.com/envoyproxy/envoy/pull/18998
Extensions::Common::Aws::AwsSigningHeaderExclusionVector{});
Extensions::Common::Aws::AwsSigningHeaderMatcherVector{},
Extensions::Common::Aws::AwsSigningHeaderMatcherVector{});

auto filter_settings = std::make_shared<FilterSettingsImpl>(
*arn, getInvocationMode(proto_config), proto_config.payload_passthrough(),
Expand Down Expand Up @@ -106,7 +107,8 @@ AwsLambdaFilterFactory::createRouteSpecificFilterConfigTyped(
service_name, region, std::move(credentials_provider), server_context,
// TODO: extend API to allow specifying header exclusion. ref:
// https://github.com/envoyproxy/envoy/pull/18998
Extensions::Common::Aws::AwsSigningHeaderExclusionVector{});
Extensions::Common::Aws::AwsSigningHeaderMatcherVector{},
Extensions::Common::Aws::AwsSigningHeaderMatcherVector{});

auto filter_settings = std::make_shared<FilterSettingsImpl>(
*arn, getInvocationMode(per_route_config.invoke_config()),
Expand Down
13 changes: 8 additions & 5 deletions source/extensions/filters/http/aws_request_signing/config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,10 @@ AwsRequestSigningFilterFactory::createSigner(
return absl::InvalidArgumentError(std::string(credentials_provider.status().message()));
}

const auto matcher_config = Extensions::Common::Aws::AwsSigningHeaderExclusionVector(
const auto include_matcher_config = Extensions::Common::Aws::AwsSigningHeaderMatcherVector(
config.match_included_headers().begin(), config.match_included_headers().end());

const auto exclude_matcher_config = Extensions::Common::Aws::AwsSigningHeaderMatcherVector(
config.match_excluded_headers().begin(), config.match_excluded_headers().end());

const bool query_string = config.has_query_string();
Expand All @@ -139,8 +142,8 @@ AwsRequestSigningFilterFactory::createSigner(

if (config.signing_algorithm() == AwsRequestSigning_SigningAlgorithm_AWS_SIGV4A) {
return std::make_unique<Extensions::Common::Aws::SigV4ASignerImpl>(
config.service_name(), region, credentials_provider.value(), server_context, matcher_config,
query_string, expiration_time);
config.service_name(), region, credentials_provider.value(), server_context,
exclude_matcher_config, include_matcher_config, query_string, expiration_time);
} else {
// Verify that we have not specified a region set when using sigv4 algorithm
if (isARegionSet(region)) {
Expand All @@ -149,8 +152,8 @@ AwsRequestSigningFilterFactory::createSigner(
"can be specified when using signing_algorithm: AWS_SIGV4A.");
}
return std::make_unique<Extensions::Common::Aws::SigV4SignerImpl>(
config.service_name(), region, credentials_provider.value(), server_context, matcher_config,
query_string, expiration_time);
config.service_name(), region, credentials_provider.value(), server_context,
exclude_matcher_config, include_matcher_config, query_string, expiration_time);
}
}

Expand Down
Loading