Skip to content

Commit

Permalink
aws: adds support for dynamically configurable credential
Browse files Browse the repository at this point in the history
Signed-off-by: Takeshi Yoneda <[email protected]>
  • Loading branch information
mathetake committed Sep 19, 2024
1 parent fdfdad0 commit 57a5ead
Show file tree
Hide file tree
Showing 11 changed files with 177 additions and 60 deletions.
1 change: 1 addition & 0 deletions api/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ proto_library(
"//envoy/extensions/clusters/dynamic_forward_proxy/v3:pkg",
"//envoy/extensions/clusters/redis/v3:pkg",
"//envoy/extensions/common/async_files/v3:pkg",
"//envoy/extensions/common/aws/v3:pkg",
"//envoy/extensions/common/dynamic_forward_proxy/v3:pkg",
"//envoy/extensions/common/matching/v3:pkg",
"//envoy/extensions/common/ratelimit/v3:pkg",
Expand Down
9 changes: 9 additions & 0 deletions api/envoy/extensions/common/aws/v3/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 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 = ["@com_github_cncf_xds//udpa/annotations:pkg"],
)
35 changes: 35 additions & 0 deletions api/envoy/extensions/common/aws/v3/credential_provider.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
syntax = "proto3";

package envoy.extensions.common.aws.v3;

import "udpa/annotations/status.proto";
import "udpa/annotations/versioning.proto";
import "validate/validate.proto";

option java_package = "io.envoyproxy.envoy.extensions.common.aws.v3";
option java_outer_classname = "AwsCommonProto";
option java_multiple_files = true;
option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/common/aws/v3;awsv3";
option (udpa.annotations.file_status).package_version_status = ACTIVE;

// [#protodoc-title: AWS common configuration]

// Configuration for AWS credential provider. Normally, this is optional and the credentials are
// retrieved from the environment or AWS configuration files by following the default credential
// provider chain. This is to support cases where the credentials need to be explicitly provided
// by the control plane.
message AwsCredentialProvider {
oneof provider {
AssumeRoleWithWebIdentity assume_role_with_web_identity = 1;
}
}

// Configuration to use `AssumeRoleWithWebIdentity <https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html>`
// to get AWS credentials.
message AssumeRoleWithWebIdentity {
// The ARN of the role to assume.
string role_arn = 1;

// The identity token that is provided by the identity provider to assume the role.
string identity_token = 2;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ licenses(["notice"]) # Apache 2

api_proto_package(
deps = [
"//envoy/extensions/common/aws/v3:pkg",
"//envoy/type/matcher/v3:pkg",
"@com_github_cncf_xds//udpa/annotations:pkg",
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ syntax = "proto3";

package envoy.extensions.filters.http.aws_request_signing.v3;

import "envoy/extensions/common/aws/v3/credential_provider.proto";
import "envoy/type/matcher/v3/string.proto";

import "google/protobuf/duration.proto";
Expand All @@ -21,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: 8]
// [#next-free-field: 9]
message AwsRequestSigning {
option (udpa.annotations.versioning).previous_message_type =
"envoy.config.filter.http.aws_request_signing.v2alpha.AwsRequestSigning";
Expand Down Expand Up @@ -107,6 +108,10 @@ message AwsRequestSigning {
// query_string: {}
//
QueryString query_string = 7;

// The credential provider for signing the request. This is optional and if not set,
// it will be retrieved from the procedure described in :ref:`config_http_filters_aws_request_signing`.
common.aws.v3.AwsCredentialProvider credential_provider = 8;
}

message AwsRequestSigningPerRoute {
Expand Down
1 change: 1 addition & 0 deletions api/versioning/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ proto_library(
"//envoy/extensions/clusters/dynamic_forward_proxy/v3:pkg",
"//envoy/extensions/clusters/redis/v3:pkg",
"//envoy/extensions/common/async_files/v3:pkg",
"//envoy/extensions/common/aws/v3:pkg",
"//envoy/extensions/common/dynamic_forward_proxy/v3:pkg",
"//envoy/extensions/common/matching/v3:pkg",
"//envoy/extensions/common/ratelimit/v3:pkg",
Expand Down
1 change: 1 addition & 0 deletions source/extensions/common/aws/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ envoy_cc_library(
"//source/common/runtime:runtime_features_lib",
"//source/common/tracing:http_tracer_lib",
"@com_google_absl//absl/time",
"@envoy_api//envoy/extensions/common/aws/v3:pkg_cc_proto",
],
)

Expand Down
88 changes: 60 additions & 28 deletions source/extensions/common/aws/credentials_provider_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -742,15 +742,16 @@ WebIdentityCredentialsProvider::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 token, absl::string_view sts_endpoint, absl::string_view role_arn,
absl::string_view role_session_name,
MetadataFetcher::MetadataReceiver::RefreshState refresh_state,
std::chrono::seconds initialization_timer, absl::string_view cluster_name = {})
: MetadataCredentialsProviderBase(
api, context, fetch_metadata_using_curl, create_metadata_fetcher_cb, cluster_name,
envoy::config::cluster::v3::Cluster::LOGICAL_DNS /*cluster_type*/, sts_endpoint,
refresh_state, initialization_timer),
token_file_path_(token_file_path), sts_endpoint_(sts_endpoint), role_arn_(role_arn),
role_session_name_(role_session_name) {}
token_file_path_(token_file_path), token_(token), sts_endpoint_(sts_endpoint),
role_arn_(role_arn), role_session_name_(role_session_name) {}

bool WebIdentityCredentialsProvider::needsRefresh() {
const auto now = api_.timeSource().systemTime();
Expand All @@ -772,11 +773,15 @@ void WebIdentityCredentialsProvider::refresh() {

ENVOY_LOG(debug, "Getting AWS web identity credentials from STS: {}", sts_endpoint_);

const auto web_token_file_or_error = api_.fileSystem().fileReadToEnd(token_file_path_);
if (!web_token_file_or_error.ok()) {
ENVOY_LOG(debug, "Unable to read AWS web identity credentials from {}", token_file_path_);
cached_credentials_ = Credentials();
return;
std::string identity_token = token_;
if (identity_token.empty()) {
const auto web_token_file_or_error = api_.fileSystem().fileReadToEnd(token_file_path_);
if (!web_token_file_or_error.ok()) {
ENVOY_LOG(debug, "Unable to read AWS web identity credentials from {}", token_file_path_);
cached_credentials_ = Credentials();
return;
}
identity_token = web_token_file_or_error.value();
}

Http::RequestMessageImpl message;
Expand All @@ -791,7 +796,7 @@ void WebIdentityCredentialsProvider::refresh() {
"&WebIdentityToken={}",
Envoy::Http::Utility::PercentEncoding::encode(role_session_name_),
Envoy::Http::Utility::PercentEncoding::encode(role_arn_),
Envoy::Http::Utility::PercentEncoding::encode(web_token_file_or_error.value())));
Envoy::Http::Utility::PercentEncoding::encode(identity_token)));
// Use the Accept header to ensure that AssumeRoleWithWebIdentityResponse is returned as JSON.
message.headers().setReference(Http::CustomHeaders::get().Accept,
Http::Headers::get().ContentTypeValues.Json);
Expand Down Expand Up @@ -915,6 +920,26 @@ Credentials CredentialsProviderChain::getCredentials() {
return Credentials();
}

std::string sessionName(Api::Api& api) {
const auto role_session_name = absl::NullSafeStringView(std::getenv(AWS_ROLE_SESSION_NAME));
std::string actual_session_name;
if (!role_session_name.empty()) {
actual_session_name = std::string(role_session_name);
} else {
// In practice, this value will be provided by the environment, so the placeholder value is
// not important. Some AWS SDKs use time in nanoseconds, so we'll just use that.
const auto now_nanos = std::chrono::duration_cast<std::chrono::nanoseconds>(
api.timeSource().systemTime().time_since_epoch())
.count();
actual_session_name = fmt::format("{}", now_nanos);
}
return actual_session_name;
}

std::string stsClusterName(absl::string_view region) {
return absl::StrCat(STS_TOKEN_CLUSTER, "-", region);
}

DefaultCredentialsProviderChain::DefaultCredentialsProviderChain(
Api::Api& api, ServerFactoryContextOptRef context, absl::string_view region,
const MetadataCredentialsProviderBase::CurlMetadataFetcher& fetch_metadata_using_curl,
Expand All @@ -936,31 +961,17 @@ DefaultCredentialsProviderChain::DefaultCredentialsProviderChain(
const auto web_token_path = absl::NullSafeStringView(std::getenv(AWS_WEB_IDENTITY_TOKEN_FILE));
const auto role_arn = absl::NullSafeStringView(std::getenv(AWS_ROLE_ARN));
if (!web_token_path.empty() && !role_arn.empty()) {
const auto role_session_name = absl::NullSafeStringView(std::getenv(AWS_ROLE_SESSION_NAME));
std::string actual_session_name;
if (!role_session_name.empty()) {
actual_session_name = std::string(role_session_name);
} else {
// In practice, this value will be provided by the environment, so the placeholder value is
// not important. Some AWS SDKs use time in nanoseconds, so we'll just use that.
const auto now_nanos = std::chrono::duration_cast<std::chrono::nanoseconds>(
api.timeSource().systemTime().time_since_epoch())
.count();
actual_session_name = fmt::format("{}", now_nanos);
}
const auto session_name = sessionName(api);
const auto sts_endpoint = Utility::getSTSEndpoint(region) + ":443";

// Handle edge case - if two web identity request signers are configured with different
// regions. This appends the region to the cluster name to differentiate the two.
auto cluster_name_ = absl::StrCat(STS_TOKEN_CLUSTER, "-", region);
const auto cluster_name = stsClusterName(region);

ENVOY_LOG(
debug,
"Using web identity credentials provider with STS endpoint: {} and session name: {}",
sts_endpoint, actual_session_name);
sts_endpoint, session_name);
add(factories.createWebIdentityCredentialsProvider(
api, context, fetch_metadata_using_curl, MetadataFetcher::create, cluster_name_,
web_token_path, sts_endpoint, role_arn, actual_session_name, refresh_state,
api, context, fetch_metadata_using_curl, MetadataFetcher::create, cluster_name,
web_token_path, "", sts_endpoint, role_arn, session_name, refresh_state,
initialization_timer));
}
}
Expand Down Expand Up @@ -1004,6 +1015,27 @@ DefaultCredentialsProviderChain::DefaultCredentialsProviderChain(
}
}

CredentialsProviderSharedPtr createCredentialsProvideFromConfig(
Server::Configuration::ServerFactoryContext& context, absl::string_view region,
const envoy::extensions::common::aws::v3::AwsCredentialProvider& config) {
if (config.has_assume_role_with_web_identity()) {
const auto& web_identity = config.assume_role_with_web_identity();
const std::string& role_arn = web_identity.role_arn();
const std::string& token = web_identity.identity_token();
const auto sts_endpoint = Utility::getSTSEndpoint(region) + ":443";
const auto cluster_name = stsClusterName(region);
const std::string role_session_name = sessionName(context.api());
const auto refresh_state = MetadataFetcher::MetadataReceiver::RefreshState::FirstRefresh;
const auto initialization_timer = std::chrono::seconds(2);
return std::make_shared<WebIdentityCredentialsProvider>(
context.api(), context, Extensions::Common::Aws::Utility::fetchMetadata,
MetadataFetcher::create, "", token, sts_endpoint, role_arn, role_session_name,
refresh_state, initialization_timer, cluster_name);
} else {
return nullptr;
}
}

} // namespace Aws
} // namespace Common
} // namespace Extensions
Expand Down
27 changes: 20 additions & 7 deletions source/extensions/common/aws/credentials_provider_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "envoy/api/api.h"
#include "envoy/common/optref.h"
#include "envoy/event/timer.h"
#include "envoy/extensions/common/aws/v3/credential_provider.pb.h"
#include "envoy/http/message.h"
#include "envoy/server/factory_context.h"

Expand Down Expand Up @@ -323,8 +324,9 @@ class WebIdentityCredentialsProvider : public MetadataCredentialsProviderBase,
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 token_file_path, absl::string_view token,
absl::string_view sts_endpoint, absl::string_view role_arn,
absl::string_view role_session_name,
MetadataFetcher::MetadataReceiver::RefreshState refresh_state,
std::chrono::seconds initialization_timer,
absl::string_view cluster_name);
Expand All @@ -334,7 +336,10 @@ class WebIdentityCredentialsProvider : public MetadataCredentialsProviderBase,
void onMetadataError(Failure reason) override;

private:
// token_ and token_file_path_ are mutually exclusive. If token_ is set, token_file_path_ is not
// used.
const std::string token_file_path_;
const std::string token_;
const std::string sts_endpoint_;
const std::string role_arn_;
const std::string role_session_name_;
Expand Down Expand Up @@ -375,8 +380,8 @@ class CredentialsProviderChainFactories {
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,
absl::string_view token_file_path, absl::string_view token, absl::string_view sts_endpoint,
absl::string_view role_arn, absl::string_view role_session_name,
MetadataFetcher::MetadataReceiver::RefreshState refresh_state,
std::chrono::seconds initialization_timer) const PURE;

Expand Down Expand Up @@ -454,17 +459,25 @@ class DefaultCredentialsProviderChain : public CredentialsProviderChain,
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,
absl::string_view token_file_path, absl::string_view token, absl::string_view sts_endpoint,
absl::string_view role_arn, absl::string_view role_session_name,
MetadataFetcher::MetadataReceiver::RefreshState refresh_state,
std::chrono::seconds initialization_timer) const override {
return std::make_shared<WebIdentityCredentialsProvider>(
api, context, fetch_metadata_using_curl, create_metadata_fetcher_cb, token_file_path,
api, context, fetch_metadata_using_curl, create_metadata_fetcher_cb, token_file_path, token,
sts_endpoint, role_arn, role_session_name, refresh_state, initialization_timer,
cluster_name);
}
};

/**
* Create an AWS credentials provider from the proto configuration instead of using the default
* credentials provider chain.
*/
CredentialsProviderSharedPtr createCredentialsProvideFromConfig(
Server::Configuration::ServerFactoryContext& context, absl::string_view region,
const envoy::extensions::common::aws::v3::AwsCredentialProvider& config);

using InstanceProfileCredentialsProviderPtr = std::shared_ptr<InstanceProfileCredentialsProvider>;
using ContainerCredentialsProviderPtr = std::shared_ptr<ContainerCredentialsProvider>;
using WebIdentityCredentialsProviderPtr = std::shared_ptr<WebIdentityCredentialsProvider>;
Expand Down
19 changes: 13 additions & 6 deletions source/extensions/filters/http/aws_request_signing/config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,13 @@ AwsRequestSigningFilterFactory::createFilterFactoryFromProtoTyped(
Extensions::Common::Aws::SignatureQueryParameterValues::DefaultExpiration);

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

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

Expand Down Expand Up @@ -122,9 +126,12 @@ AwsRequestSigningFilterFactory::createRouteSpecificFilterConfigTyped(
per_route_config.aws_request_signing().query_string(), expiration_time, 5);

auto credentials_provider =
std::make_shared<Extensions::Common::Aws::DefaultCredentialsProviderChain>(
context.api(), makeOptRef(context), region,
Extensions::Common::Aws::Utility::fetchMetadata);
per_route_config.aws_request_signing().has_credential_provider()
? Extensions::Common::Aws::createCredentialsProvideFromConfig(
context, region, per_route_config.aws_request_signing().credential_provider())
: std::make_shared<Extensions::Common::Aws::DefaultCredentialsProviderChain>(
context.api(), makeOptRef(context), region,
Extensions::Common::Aws::Utility::fetchMetadata);

const auto matcher_config = Extensions::Common::Aws::AwsSigningHeaderExclusionVector(
per_route_config.aws_request_signing().match_excluded_headers().begin(),
Expand Down
Loading

0 comments on commit 57a5ead

Please sign in to comment.