Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

aws_signing: support for dynamically configurable credential #36217

Merged
merged 16 commits into from
Oct 21, 2024
Merged
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
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"],
)
54 changes: 54 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,54 @@
syntax = "proto3";

package envoy.extensions.common.aws.v3;

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

option java_package = "io.envoyproxy.envoy.extensions.common.aws.v3";
option java_outer_classname = "CredentialProviderProto";
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 {
// The option to use `AssumeRoleWithWebIdentity <https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html>`_.
// If inline_credential is set, this is ignored.
AssumeRoleWithWebIdentityCredentialProvider assume_role_with_web_identity = 1;

// The option to use an inline credential.
// If this is set, it takes precedence over assume_role_with_web_identity.
InlineCredentialProvider inline_credential = 2;
}

// Configuration to use an inline AWS credential. This is an equivalent to setting the well-known
// environment variables ``AWS_ACCESS_KEY_ID``, ``AWS_SECRET_ACCESS_KEY``, and the optional ``AWS_SESSION_TOKEN``.
message InlineCredentialProvider {
// The AWS access key ID.
string access_key_id = 1 [(validate.rules).string = {min_len: 1}];

// The AWS secret access key.
string secret_access_key = 2
[(validate.rules).string = {min_len: 1}, (udpa.annotations.sensitive) = true];

// The AWS session token. This is optional.
string session_token = 3 [(udpa.annotations.sensitive) = true];
}

// Configuration to use `AssumeRoleWithWebIdentity <https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html>`_
// to get AWS credentials.
message AssumeRoleWithWebIdentityCredentialProvider {
// The ARN of the role to assume.
string role_arn = 1 [(validate.rules).string = {min_len: 1}];

// The web identity token that is provided by the identity provider to assume the role.
string web_identity_token = 2
[(validate.rules).string = {min_len: 1}, (udpa.annotations.sensitive) = true];
}
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
5 changes: 5 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ removed_config_or_runtime:
# *Normally occurs at the end of the* :ref:`deprecation period <deprecated>`

new_features:
- area: aws_request_signing
change: |
Added an optional field :ref:`credential_provider
<envoy_v3_api_field_extensions.filters.http.aws_request_signing.v3.AwsRequestSigning.credential_provider>`
to the AWS request signing filter to explicitly specify a source for AWS credentials.
- area: tls
change: |
Added support for P-384 and P-521 curves for TLS server certificates.
Expand Down
1 change: 1 addition & 0 deletions docs/root/api-v3/common_messages/common_messages.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Common messages
../extensions/network/socket_interface/v3/default_socket_interface.proto
../extensions/matching/common_inputs/environment_variable/v3/input.proto
../config/core/v3/extension.proto
../extensions/common/aws/v3/credential_provider.proto
../extensions/common/matching/v3/extension_matcher.proto
../extensions/filters/common/dependency/v3/dependency.proto
../extensions/regex_engines/v3/google_re2.proto
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Credentials
-----------

The filter uses a number of different credentials providers to obtain an AWS access key ID, AWS secret access key, and AWS session token.
It moves through the credentials providers in the order described below, stopping when one of them returns an access key ID and a
By default, it moves through the credentials providers in the order described below, stopping when one of them returns an access key ID and a
secret access key (the session token is optional).

1. Environment variables. The environment variables ``AWS_ACCESS_KEY_ID``, ``AWS_SECRET_ACCESS_KEY``, and ``AWS_SESSION_TOKEN`` are used.
Expand Down Expand Up @@ -46,6 +46,9 @@ secret access key (the session token is optional).
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.

Alternatively, each AWS filter (either AWS Request Signing or AWS Lambda) has its own optional configuration to specify the source of the credentials. For example, AWS Request Signing filter
has :ref:`credential_provider <envoy_v3_api_field_extensions.filters.http.aws_request_signing.v3.AwsRequestSigning.credential_provider>` field.

Statistics
----------

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
95 changes: 67 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,34 @@ DefaultCredentialsProviderChain::DefaultCredentialsProviderChain(
}
}

absl::StatusOr<CredentialsProviderSharedPtr> createCredentialsProviderFromConfig(
Server::Configuration::ServerFactoryContext& context, absl::string_view region,
const envoy::extensions::common::aws::v3::AwsCredentialProvider& config) {
// The precedence order is: inline_credential > assume_role_with_web_identity.
if (config.has_inline_credential()) {
const auto& inline_credential = config.inline_credential();
return std::make_shared<InlineCredentialProvider>(inline_credential.access_key_id(),
inline_credential.secret_access_key(),
inline_credential.session_token());
} else 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.web_identity_token();
const std::string sts_endpoint = Utility::getSTSEndpoint(region) + ":443";
const std::string cluster_name = stsClusterName(region);
const std::string role_session_name = sessionName(context.api());
const auto refresh_state = MetadataFetcher::MetadataReceiver::RefreshState::FirstRefresh;
// This "two seconds" is a bit arbitrary, but matches the other places in the codebase.
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 absl::InvalidArgumentError("No AWS credential provider specified");
}
}

} // namespace Aws
} // namespace Common
} // namespace Extensions
Expand Down
Loading