Skip to content
Merged
8 changes: 8 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,14 @@ removed_config_or_runtime:
runtime flag and legacy code path.

new_features:
- area: aws
change: |
Added support for AWS common utility to fetch metadata credentials from AWS STS by using ``WebIdentityToken``. To enable
you need to set ``envoy.reloadable_features.use_http_client_to_fetch_aws_credentials`` to ``true`` so that web identity
credentials provider can use http async client to fetch credentials. Web identity credentials provider cannot use current
default libcurl credentials fetcher which is under deprecation and will soon be removed. Web identity credentials provider
is not compatible with :ref:`Grpc Credentials AWS IAM <envoy_v3_api_file_envoy/config/grpc_credential/v3/aws_iam.proto>`
plugin which can only support deprecated libcurl credentials fetcher, see https://github.com/envoyproxy/envoy/pull/30626.
- area: filters
change: |
Added :ref:`the Basic Auth filter <envoy_v3_api_msg_extensions.filters.http.basic_auth.v3.BasicAuth>`, which can be used to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,19 @@ secret access key (the session token is optional).
the file ``~/.aws/credentials`` and profile ``default`` are used. The fields ``aws_access_key_id``, ``aws_secret_access_key``, and
``aws_session_token`` defined for the profile in the credentials file are used. These credentials are cached for 1 hour.

3. Either EC2 instance metadata or ECS task metadata. For EC2 instance metadata, the fields ``AccessKeyId``, ``SecretAccessKey``, and
3. From `AssumeRoleWithWebIdentity <https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html>`_ API call
towards AWS Security Token Service using ``WebIdentityToken`` read from a file pointed by ``AWS_WEB_IDENTITY_TOKEN_FILE`` environment
variable and role arn read from ``AWS_ROLE_ARN`` environment variable. The credentials are extracted from the fields ``AccessKeyId``,
``SecretAccessKey``, and ``SessionToken`` are used, and credentials are cached for 1 hour or until they expire (according to the field
``Expiration``). To enable this credentials provider set ``envoy.reloadable_features.use_http_client_to_fetch_aws_credentials`` to ``true``
so that it can use http async client to fetch the credentials. This provider is not compatible with :ref:`Grpc Credentials AWS AwsIamConfig
<envoy_v3_api_file_envoy/config/grpc_credential/v3/aws_iam.proto>` plugin which can only support deprecated libcurl credentials fetcher
, see https://github.com/envoyproxy/envoy/pull/30626. To fetch the credentials a static cluster is required with the name
``sts_token_service_internal`` pointing towards regional AWS Security Token Service. The static internal cluster will still be added even
if initially ``envoy.reloadable_features.use_http_client_to_fetch_aws_credentials`` is not set so that subsequently if the reloadable feature
is set to ``true`` the cluster config is available to fetch the credentials.

4. Either EC2 instance metadata or ECS task metadata. For EC2 instance metadata, the fields ``AccessKeyId``, ``SecretAccessKey``, and
``Token`` are used, and credentials are cached for 1 hour. For ECS task metadata, the fields ``AccessKeyId``, ``SecretAccessKey``, and
``Token`` are used, and credentials are cached for 1 hour or until they expire (according to the field ``Expiration``). Note that the
latest update on AWS credentials provider utility provides an option to use http async client functionality instead of libcurl to fetch the
Expand Down
6 changes: 6 additions & 0 deletions source/extensions/common/aws/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ load(
"//bazel:envoy_build_system.bzl",
"envoy_cc_library",
"envoy_extension_package",
"envoy_select_boringssl",
)

licenses(["notice"]) # Apache 2
Expand Down Expand Up @@ -76,6 +77,11 @@ envoy_cc_library(
name = "utility_lib",
srcs = ["utility.cc"],
hdrs = ["utility.h"],
copts = envoy_select_boringssl(
[
"-DENVOY_SSL_FIPS",
],
),
external_deps = ["curl"],
deps = [
"//envoy/http:message_interface",
Expand Down
226 changes: 212 additions & 14 deletions source/extensions/common/aws/credentials_provider_impl.cc

Large diffs are not rendered by default.

68 changes: 61 additions & 7 deletions source/extensions/common/aws/credentials_provider_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,11 @@ class MetadataCredentialsProviderBase : public CachedCredentialsProviderBase {
using CurlMetadataFetcher = std::function<absl::optional<std::string>(Http::RequestMessage&)>;
using OnAsyncFetchCb = std::function<void(const std::string&&)>;

MetadataCredentialsProviderBase(Api::Api& api, ServerFactoryContextOptRef context,
const CurlMetadataFetcher& fetch_metadata_using_curl,
CreateMetadataFetcherCb create_metadata_fetcher_cb,
absl::string_view cluster_name, absl::string_view uri);
MetadataCredentialsProviderBase(
Api::Api& api, ServerFactoryContextOptRef context,
const CurlMetadataFetcher& fetch_metadata_using_curl,
CreateMetadataFetcherCb create_metadata_fetcher_cb, absl::string_view cluster_name,
const envoy::config::cluster::v3::Cluster::DiscoveryType cluster_type, absl::string_view uri);

Credentials getCredentials() override;

Expand Down Expand Up @@ -127,6 +128,10 @@ class MetadataCredentialsProviderBase : public CachedCredentialsProviderBase {
CreateMetadataFetcherCb create_metadata_fetcher_cb_;
// The cluster name to use for internal static cluster pointing towards the credentials provider.
const std::string cluster_name_;
// The cluster type to use for internal static cluster pointing towards the credentials provider.
const envoy::config::cluster::v3::Cluster::DiscoveryType cluster_type_;
// The uri of internal static cluster credentials provider.
const std::string uri_;
// The cache duration of the fetched credentials.
const std::chrono::seconds cache_duration_;
// The thread local slot for cache.
Expand Down Expand Up @@ -218,6 +223,36 @@ class TaskRoleCredentialsProvider : public MetadataCredentialsProviderBase,
void extractCredentials(const std::string&& credential_document_value);
};

/**
* Retrieve AWS credentials from Security Token Service using a web identity token (e.g. OAuth,
* OpenID)
*/
class WebIdentityCredentialsProvider : public MetadataCredentialsProviderBase,
public MetadataFetcher::MetadataReceiver {
public:
WebIdentityCredentialsProvider(Api::Api& api, ServerFactoryContextOptRef context,
const CurlMetadataFetcher& fetch_metadata_using_curl,
CreateMetadataFetcherCb create_metadata_fetcher_cb,
absl::string_view token_file_path, absl::string_view sts_endpoint,
absl::string_view role_arn, absl::string_view role_session_name,
absl::string_view cluster_name);

// Following functions are for MetadataFetcher::MetadataReceiver interface
void onMetadataSuccess(const std::string&& body) override;
void onMetadataError(Failure reason) override;

private:
SystemTime expiration_time_;
const std::string token_file_path_;
const std::string sts_endpoint_;
const std::string role_arn_;
const std::string role_session_name_;

bool needsRefresh() override;
void refresh() override;
void extractCredentials(const std::string&& credential_document_value);
};

/**
* AWS credentials provider chain, able to fallback between multiple credential providers.
*/
Expand Down Expand Up @@ -245,6 +280,13 @@ class CredentialsProviderChainFactories {
virtual CredentialsProviderSharedPtr
createCredentialsFileCredentialsProvider(Api::Api& api) const PURE;

virtual CredentialsProviderSharedPtr createWebIdentityCredentialsProvider(
Api::Api& api, ServerFactoryContextOptRef context,
const MetadataCredentialsProviderBase::CurlMetadataFetcher& fetch_metadata_using_curl,
CreateMetadataFetcherCb create_metadata_fetcher_cb, absl::string_view cluster_name,
absl::string_view token_file_path, absl::string_view sts_endpoint, absl::string_view role_arn,
absl::string_view role_session_name) const PURE;

virtual CredentialsProviderSharedPtr createTaskRoleCredentialsProvider(
Api::Api& api, ServerFactoryContextOptRef context,
const MetadataCredentialsProviderBase::CurlMetadataFetcher& fetch_metadata_using_curl,
Expand All @@ -268,12 +310,12 @@ class DefaultCredentialsProviderChain : public CredentialsProviderChain,
public CredentialsProviderChainFactories {
public:
DefaultCredentialsProviderChain(
Api::Api& api, ServerFactoryContextOptRef context,
Api::Api& api, ServerFactoryContextOptRef context, absl::string_view region,
const MetadataCredentialsProviderBase::CurlMetadataFetcher& fetch_metadata_using_curl)
: DefaultCredentialsProviderChain(api, context, fetch_metadata_using_curl, *this) {}
: DefaultCredentialsProviderChain(api, context, region, fetch_metadata_using_curl, *this) {}

DefaultCredentialsProviderChain(
Api::Api& api, ServerFactoryContextOptRef context,
Api::Api& api, ServerFactoryContextOptRef context, absl::string_view region,
const MetadataCredentialsProviderBase::CurlMetadataFetcher& fetch_metadata_using_curl,
const CredentialsProviderChainFactories& factories);

Expand Down Expand Up @@ -305,10 +347,22 @@ class DefaultCredentialsProviderChain : public CredentialsProviderChain,
return std::make_shared<InstanceProfileCredentialsProvider>(
api, context, fetch_metadata_using_curl, create_metadata_fetcher_cb, cluster_name);
}

CredentialsProviderSharedPtr createWebIdentityCredentialsProvider(
Api::Api& api, ServerFactoryContextOptRef context,
const MetadataCredentialsProviderBase::CurlMetadataFetcher& fetch_metadata_using_curl,
CreateMetadataFetcherCb create_metadata_fetcher_cb, absl::string_view cluster_name,
absl::string_view token_file_path, absl::string_view sts_endpoint, absl::string_view role_arn,
absl::string_view role_session_name) const override {
return std::make_shared<WebIdentityCredentialsProvider>(
api, context, fetch_metadata_using_curl, create_metadata_fetcher_cb, token_file_path,
sts_endpoint, role_arn, role_session_name, cluster_name);
}
};

using InstanceProfileCredentialsProviderPtr = std::shared_ptr<InstanceProfileCredentialsProvider>;
using TaskRoleCredentialsProviderPtr = std::shared_ptr<TaskRoleCredentialsProvider>;
using WebIdentityCredentialsProviderPtr = std::shared_ptr<WebIdentityCredentialsProvider>;

} // namespace Aws
} // namespace Common
Expand Down
19 changes: 12 additions & 7 deletions source/extensions/common/aws/metadata_fetcher.cc
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ class MetadataFetcherImpl : public MetadataFetcher,
void fetch(Http::RequestMessage& message, Tracing::Span& parent_span,
MetadataFetcher::MetadataReceiver& receiver) override {
ASSERT(!request_);
ASSERT(!receiver_);
complete_ = false;
receiver_ = makeOptRef(receiver);
const auto thread_local_cluster = cm_.getThreadLocalCluster(cluster_name_);
Expand All @@ -70,8 +69,15 @@ class MetadataFetcherImpl : public MetadataFetcher,
const auto path = message.headers().getPathValue();
const auto scheme = message.headers().getSchemeValue();
const auto method = message.headers().getMethodValue();
ENVOY_LOG(debug, "fetch AWS Metadata at [uri = {}]: start from cluster {}",
fmt::format("{}://{}{}", scheme, host, path), cluster_name_);

const size_t query_offset = path.find('?');
// Sanitize the path before logging.
// However, the route debug log will still display the entire path.
// So safely store the Envoy logs at debug level.
const absl::string_view sanitized_path =
query_offset != absl::string_view::npos ? path.substr(0, query_offset) : path;
ENVOY_LOG(debug, "fetch AWS Metadata from the cluster {} at [uri = {}]", cluster_name_,
fmt::format("{}://{}{}", scheme, host, sanitized_path));

Http::RequestHeaderMapPtr headersPtr =
Envoy::Http::createHeaderMap<Envoy::Http::RequestHeaderMapImpl>(
Expand Down Expand Up @@ -116,6 +122,7 @@ class MetadataFetcherImpl : public MetadataFetcher,

// HTTP async receive method on success.
void onSuccess(const Http::AsyncClient::Request&, Http::ResponseMessagePtr&& response) override {
ASSERT(receiver_);
complete_ = true;
const uint64_t status_code = Http::Utility::getResponseStatus(response->headers());
if (status_code == enumToInt(Http::Code::OK)) {
Expand Down Expand Up @@ -145,6 +152,7 @@ class MetadataFetcherImpl : public MetadataFetcher,
// HTTP async receive method on failure.
void onFailure(const Http::AsyncClient::Request&,
Http::AsyncClient::FailureReason reason) override {
ASSERT(receiver_);
ENVOY_LOG(debug, "{}: fetch AWS Metadata [cluster = {}]: network error {}", __func__,
cluster_name_, enumToInt(reason));
complete_ = true;
Expand All @@ -162,10 +170,7 @@ class MetadataFetcherImpl : public MetadataFetcher,
OptRef<MetadataFetcher::MetadataReceiver> receiver_;
OptRef<Http::AsyncClient::Request> request_;

void reset() {
request_.reset();
receiver_.reset();
}
void reset() { request_.reset(); }
};
} // namespace

Expand Down
37 changes: 34 additions & 3 deletions source/extensions/common/aws/utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,22 @@ Utility::joinCanonicalHeaderNames(const std::map<std::string, std::string>& cano
});
}

std::string Utility::getSTSEndpoint(absl::string_view region) {
if (region == "cn-northwest-1" || region == "cn-north-1") {
return fmt::format("sts.{}.amazonaws.com.cn", region);
}
#ifdef ENVOY_SSL_FIPS
Comment thread
suniltheta marked this conversation as resolved.
// Use AWS STS FIPS endpoints in FIPS mode https://docs.aws.amazon.com/general/latest/gr/sts.html.
// Note: AWS GovCloud doesn't have separate fips endpoints.
// TODO(suniltheta): Include `ca-central-1` when sts supports a dedicated FIPS endpoint.
if (region == "us-east-1" || region == "us-east-2" || region == "us-west-1" ||
region == "us-west-2") {
return fmt::format("sts-fips.{}.amazonaws.com", region);
}
#endif
return fmt::format("sts.{}.amazonaws.com", region);
}

static size_t curlCallback(char* ptr, size_t, size_t nmemb, void* data) {
auto buf = static_cast<std::string*>(data);
buf->append(ptr, nmemb);
Expand All @@ -241,8 +257,9 @@ absl::optional<std::string> Utility::fetchMetadata(Http::RequestMessage& message
const auto host = message.headers().getHostValue();
const auto path = message.headers().getPathValue();
const auto method = message.headers().getMethodValue();
const auto scheme = message.headers().getSchemeValue();

const std::string url = fmt::format("http://{}{}", host, path);
const std::string url = fmt::format("{}://{}{}", scheme, host, path);
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_TIMEOUT, TIMEOUT.count());
curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L);
Expand Down Expand Up @@ -334,12 +351,26 @@ bool Utility::addInternalClusterStatic(
["envoy.extensions.upstreams.http.v3.HttpProtocolOptions"]
.PackFrom(protocol_options);

// Add tls transport socket if cluster supports https over port 443.
if (port == 443) {
auto* socket = cluster.mutable_transport_socket();
envoy::extensions::transport_sockets::tls::v3::UpstreamTlsContext tls_socket;
socket->set_name("envoy.transport_sockets.tls");
socket->mutable_typed_config()->PackFrom(tls_socket);
}

// TODO(suniltheta): use random number generator here for cluster version.
// While adding multiple clusters make sure that change in random version number across
// multiple clusters won't make Envoy delete/replace previously registered internal cluster.
cm.addOrUpdateCluster(cluster, "12345");

const auto cluster_type_str = envoy::config::cluster::v3::Cluster::DiscoveryType_descriptor()
->FindValueByNumber(cluster_type)
->name();
ENVOY_LOG_MISC(info,
"Added a {} internal cluster [name: {}, address:{}:{}] to fetch aws "
"Added a {} internal cluster [name: {}, address:{}] to fetch aws "
"credentials",
cluster_type, cluster_name, host, port);
cluster_type_str, cluster_name, host_port);
}
END_TRY
CATCH(const EnvoyException& e, {
Expand Down
8 changes: 8 additions & 0 deletions source/extensions/common/aws/utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ class Utility {
static std::string
joinCanonicalHeaderNames(const std::map<std::string, std::string>& canonical_headers);

/**
* Get the Security Token Service endpoint for a given region: sts.<region>.amazonaws.com
* See: https://docs.aws.amazon.com/general/latest/gr/rande.html#sts_region
* @param region An AWS region.
* @return an sts endpoint url.
*/
static std::string getSTSEndpoint(absl::string_view region);

/**
* Fetch AWS instance or task metadata.
*
Expand Down
2 changes: 1 addition & 1 deletion source/extensions/filters/http/aws_lambda/config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Http::FilterFactoryCb AwsLambdaFilterFactory::createFilterFactoryFromProtoTyped(

auto credentials_provider =
std::make_shared<Extensions::Common::Aws::DefaultCredentialsProviderChain>(
server_context.api(), makeOptRef(server_context),
server_context.api(), makeOptRef(server_context), region,
Extensions::Common::Aws::Utility::fetchMetadata);

auto signer = std::make_shared<Extensions::Common::Aws::SignerImpl>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Http::FilterFactoryCb AwsRequestSigningFilterFactory::createFilterFactoryFromPro

auto credentials_provider =
std::make_shared<Extensions::Common::Aws::DefaultCredentialsProviderChain>(
server_context.api(), makeOptRef(server_context),
server_context.api(), makeOptRef(server_context), config.region(),
Extensions::Common::Aws::Utility::fetchMetadata);
const auto matcher_config = Extensions::Common::Aws::AwsSigV4HeaderExclusionVector(
config.match_excluded_headers().begin(), config.match_excluded_headers().end());
Expand All @@ -45,7 +45,8 @@ AwsRequestSigningFilterFactory::createRouteSpecificFilterConfigTyped(
Server::Configuration::ServerFactoryContext& context, ProtobufMessage::ValidationVisitor&) {
auto credentials_provider =
std::make_shared<Extensions::Common::Aws::DefaultCredentialsProviderChain>(
context.api(), makeOptRef(context), Extensions::Common::Aws::Utility::fetchMetadata);
context.api(), makeOptRef(context), per_route_config.aws_request_signing().region(),
Extensions::Common::Aws::Utility::fetchMetadata);
const auto matcher_config = Extensions::Common::Aws::AwsSigV4HeaderExclusionVector(
per_route_config.aws_request_signing().match_excluded_headers().begin(),
per_route_config.aws_request_signing().match_excluded_headers().end());
Expand Down
6 changes: 4 additions & 2 deletions source/extensions/grpc_credentials/aws_iam/config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,18 @@ std::shared_ptr<grpc::ChannelCredentials> AwsIamGrpcCredentialsFactory::getChann
const auto& config = Envoy::MessageUtil::downcastAndValidate<
const envoy::config::grpc_credential::v3::AwsIamConfig&>(
*config_message, ProtobufMessage::getNullValidationVisitor());
const auto region = getRegion(config);
// TODO(suniltheta): Due to the reasons explained in
// https://github.com/envoyproxy/envoy/issues/27586 this aws iam plugin is not able to
// utilize http async client to fetch AWS credentials. For time being this is still using
// libcurl to fetch the credentials. To fully get rid of curl, need to address the below
// usage of AWS credentials common utils. Until then we are setting nullopt for server
// factory context.
auto credentials_provider = std::make_shared<Common::Aws::DefaultCredentialsProviderChain>(
api, absl::nullopt /*Empty factory context*/, Common::Aws::Utility::fetchMetadata);
api, absl::nullopt /*Empty factory context*/, region,
Common::Aws::Utility::fetchMetadata);
auto signer = std::make_unique<Common::Aws::SignerImpl>(
config.service_name(), getRegion(config), credentials_provider, api.timeSource(),
config.service_name(), region, credentials_provider, api.timeSource(),
// TODO: extend API to allow specifying header exclusion. ref:
// https://github.com/envoyproxy/envoy/pull/18998
Common::Aws::AwsSigV4HeaderExclusionVector{});
Expand Down
Loading