-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Added a set of AWS credential providers. #7281
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,34 +13,57 @@ namespace HttpFilters { | |
| namespace Common { | ||
| namespace Aws { | ||
|
|
||
| /** | ||
| * AWS credentials container | ||
| * | ||
| * If a credential component was not found in the execution environment, it's getter method will | ||
| * return absl::nullopt. Credential components with the empty string value are treated as not found. | ||
| */ | ||
| class Credentials { | ||
| public: | ||
| Credentials() = default; | ||
|
|
||
| Credentials(absl::string_view access_key_id, absl::string_view secret_access_key) | ||
| : access_key_id_(access_key_id), secret_access_key_(secret_access_key) {} | ||
|
|
||
| Credentials(absl::string_view access_key_id, absl::string_view secret_access_key, | ||
| absl::string_view session_token) | ||
| : access_key_id_(access_key_id), secret_access_key_(secret_access_key), | ||
| session_token_(session_token) {} | ||
| Credentials(absl::string_view access_key_id = absl::string_view(), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Defaulting to
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I already have this constructor, I wouldn't get the default one unless I request it with |
||
| absl::string_view secret_access_key = absl::string_view(), | ||
| absl::string_view session_token = absl::string_view()) { | ||
| if (!access_key_id.empty()) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this mean we always need
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Credentials can be one of:
Implication of an empty access key is that request will be made with anonymous identity. For example requests can be made to S3 with anonymous identity if bucket is configured in that way. Non-production environments can also accept anonymous requests. |
||
| access_key_id_ = std::string(access_key_id); | ||
| if (!secret_access_key.empty()) { | ||
| secret_access_key_ = std::string(secret_access_key); | ||
| if (!session_token.empty()) { | ||
| session_token_ = std::string(session_token); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const absl::optional<std::string>& accessKeyId() const { return access_key_id_; } | ||
|
|
||
| const absl::optional<std::string>& secretAccessKey() const { return secret_access_key_; } | ||
|
|
||
| const absl::optional<std::string>& sessionToken() const { return session_token_; } | ||
|
|
||
| bool operator==(const Credentials& other) const { | ||
| return access_key_id_ == other.access_key_id_ && | ||
| secret_access_key_ == other.secret_access_key_ && session_token_ == other.session_token_; | ||
| } | ||
|
|
||
| private: | ||
| absl::optional<std::string> access_key_id_; | ||
| absl::optional<std::string> secret_access_key_; | ||
| absl::optional<std::string> session_token_; | ||
| }; | ||
|
|
||
| /** | ||
| * Interface for classes able to fetch AWS credentials from the execution environment. | ||
| */ | ||
| class CredentialsProvider { | ||
| public: | ||
| virtual ~CredentialsProvider() = default; | ||
|
|
||
| /** | ||
| * Get credentials from the environment. | ||
| * | ||
| * @return AWS credentials | ||
| */ | ||
| virtual Credentials getCredentials() PURE; | ||
| }; | ||
|
|
||
|
|
@@ -50,4 +73,4 @@ using CredentialsProviderSharedPtr = std::shared_ptr<CredentialsProvider>; | |
| } // namespace Common | ||
| } // namespace HttpFilters | ||
| } // namespace Extensions | ||
| } // namespace Envoy | ||
| } // namespace Envoy | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,220 @@ | ||
| #include "extensions/filters/http/common/aws/credentials_provider_impl.h" | ||
|
|
||
| #include "envoy/common/exception.h" | ||
|
|
||
| #include "common/common/lock_guard.h" | ||
| #include "common/http/utility.h" | ||
| #include "common/json/json_loader.h" | ||
|
|
||
| namespace Envoy { | ||
| namespace Extensions { | ||
| namespace HttpFilters { | ||
| namespace Common { | ||
| namespace Aws { | ||
|
|
||
| constexpr static char AWS_ACCESS_KEY_ID[] = "AWS_ACCESS_KEY_ID"; | ||
| constexpr static char AWS_SECRET_ACCESS_KEY[] = "AWS_SECRET_ACCESS_KEY"; | ||
| constexpr static char AWS_SESSION_TOKEN[] = "AWS_SESSION_TOKEN"; | ||
|
|
||
| constexpr static char ACCESS_KEY_ID[] = "AccessKeyId"; | ||
| constexpr static char SECRET_ACCESS_KEY[] = "SecretAccessKey"; | ||
| constexpr static char TOKEN[] = "Token"; | ||
| constexpr static char EXPIRATION[] = "Expiration"; | ||
| constexpr static char EXPIRATION_FORMAT[] = "%E4Y%m%dT%H%M%S%z"; | ||
| constexpr static char TRUE[] = "true"; | ||
|
|
||
| constexpr static char AWS_CONTAINER_CREDENTIALS_RELATIVE_URI[] = | ||
| "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"; | ||
| constexpr static char AWS_CONTAINER_CREDENTIALS_FULL_URI[] = "AWS_CONTAINER_CREDENTIALS_FULL_URI"; | ||
| constexpr static char AWS_CONTAINER_AUTHORIZATION_TOKEN[] = "AWS_CONTAINER_AUTHORIZATION_TOKEN"; | ||
| constexpr static char AWS_EC2_METADATA_DISABLED[] = "AWS_EC2_METADATA_DISABLED"; | ||
|
|
||
| constexpr static std::chrono::hours REFRESH_INTERVAL{1}; | ||
| constexpr static std::chrono::seconds REFRESH_GRACE_PERIOD{5}; | ||
| constexpr static char EC2_METADATA_HOST[] = "169.254.169.254:80"; | ||
| constexpr static char CONTAINER_METADATA_HOST[] = "169.254.170.2:80"; | ||
| constexpr static char SECURITY_CREDENTIALS_PATH[] = "/latest/meta-data/iam/security-credentials"; | ||
|
|
||
| Credentials EnvironmentCredentialsProvider::getCredentials() { | ||
| ENVOY_LOG(debug, "Getting AWS credentials from the environment"); | ||
|
|
||
| const auto access_key_id = std::getenv(AWS_ACCESS_KEY_ID); | ||
| if (access_key_id == nullptr) { | ||
| return Credentials(); | ||
| } | ||
|
|
||
| const auto secret_access_key = std::getenv(AWS_SECRET_ACCESS_KEY); | ||
| const auto session_token = std::getenv(AWS_SESSION_TOKEN); | ||
|
|
||
| ENVOY_LOG(debug, "Found following AWS credentials in the environment: {}={}, {}={}, {}={}", | ||
| AWS_ACCESS_KEY_ID, access_key_id ? access_key_id : "", AWS_SECRET_ACCESS_KEY, | ||
| secret_access_key ? "*****" : "", AWS_SESSION_TOKEN, session_token ? "*****" : ""); | ||
|
|
||
| return Credentials(access_key_id, secret_access_key, session_token); | ||
| } | ||
|
|
||
| void MetadataCredentialsProviderBase::refreshIfNeeded() { | ||
| const Thread::LockGuard lock(lock_); | ||
| if (needsRefresh()) { | ||
| refresh(); | ||
| } | ||
| } | ||
|
|
||
| bool InstanceProfileCredentialsProvider::needsRefresh() { | ||
| return api_.timeSource().systemTime() - last_updated_ > REFRESH_INTERVAL; | ||
| } | ||
|
|
||
| void InstanceProfileCredentialsProvider::refresh() { | ||
| ENVOY_LOG(debug, "Getting AWS credentials from the instance metadata"); | ||
|
|
||
| // First discover the Role of this instance | ||
| const auto instance_role_string = | ||
| metadata_fetcher_(EC2_METADATA_HOST, SECURITY_CREDENTIALS_PATH, ""); | ||
| if (!instance_role_string) { | ||
| ENVOY_LOG(error, "Could not retrieve credentials listing from the instance metadata"); | ||
| return; | ||
| } | ||
|
|
||
| const auto instance_role_list = | ||
| StringUtil::splitToken(StringUtil::trim(instance_role_string.value()), "\n"); | ||
| if (instance_role_list.empty()) { | ||
| ENVOY_LOG(error, "No AWS credentials were found in the instance metadata"); | ||
| return; | ||
| } | ||
| ENVOY_LOG(debug, "AWS credentials list:\n{}", instance_role_string.value()); | ||
|
|
||
| // Only one Role can be associated with an instance: | ||
| // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html | ||
| const auto credential_path = | ||
| std::string(SECURITY_CREDENTIALS_PATH) + "/" + | ||
| std::string(instance_role_list[0].data(), instance_role_list[0].size()); | ||
| ENVOY_LOG(debug, "AWS credentials path: {}", credential_path); | ||
|
|
||
| // Then fetch and parse the credentials | ||
| const auto credential_document = metadata_fetcher_(EC2_METADATA_HOST, credential_path, ""); | ||
| if (!credential_document) { | ||
| ENVOY_LOG(error, "Could not load AWS credentials document from the instance metadata"); | ||
| return; | ||
| } | ||
|
|
||
| Json::ObjectSharedPtr document_json; | ||
| try { | ||
| document_json = Json::Factory::loadFromString(credential_document.value()); | ||
| } catch (EnvoyException& e) { | ||
| ENVOY_LOG(error, "Could not parse AWS credentials document: {}", e.what()); | ||
| return; | ||
| } | ||
|
|
||
| const auto access_key_id = document_json->getString(ACCESS_KEY_ID, ""); | ||
| const auto secret_access_key = document_json->getString(SECRET_ACCESS_KEY, ""); | ||
| const auto session_token = document_json->getString(TOKEN, ""); | ||
|
|
||
| ENVOY_LOG(debug, "Found following AWS credentials in the instance metadata: {}={}, {}={}, {}={}", | ||
| AWS_ACCESS_KEY_ID, access_key_id, AWS_SECRET_ACCESS_KEY, | ||
| secret_access_key.empty() ? "" : "*****", AWS_SESSION_TOKEN, | ||
| session_token.empty() ? "" : "*****"); | ||
|
|
||
| cached_credentials_ = Credentials(access_key_id, secret_access_key, session_token); | ||
| last_updated_ = api_.timeSource().systemTime(); | ||
| } | ||
|
|
||
| bool TaskRoleCredentialsProvider::needsRefresh() { | ||
| const auto now = api_.timeSource().systemTime(); | ||
| return (now - last_updated_ > REFRESH_INTERVAL) || | ||
| (expiration_time_ - now < REFRESH_GRACE_PERIOD); | ||
| } | ||
|
|
||
| void TaskRoleCredentialsProvider::refresh() { | ||
| ENVOY_LOG(debug, "Getting AWS credentials from the task role at URI: {}", credential_uri_); | ||
|
|
||
| absl::string_view host; | ||
| absl::string_view path; | ||
| Http::Utility::extractHostPathFromUri(credential_uri_, host, path); | ||
| const auto credential_document = | ||
| metadata_fetcher_(std::string(host.data(), host.size()), | ||
| std::string(path.data(), path.size()), authorization_token_); | ||
| if (!credential_document) { | ||
| ENVOY_LOG(error, "Could not load AWS credentials document from the task role"); | ||
| return; | ||
| } | ||
|
|
||
| Json::ObjectSharedPtr document_json; | ||
| try { | ||
| document_json = Json::Factory::loadFromString(credential_document.value()); | ||
| } catch (EnvoyException& e) { | ||
| ENVOY_LOG(error, "Could not parse AWS credentials document from the task role: {}", e.what()); | ||
| return; | ||
| } | ||
|
|
||
| const auto access_key_id = document_json->getString(ACCESS_KEY_ID, ""); | ||
| const auto secret_access_key = document_json->getString(SECRET_ACCESS_KEY, ""); | ||
| const auto session_token = document_json->getString(TOKEN, ""); | ||
|
|
||
| ENVOY_LOG(debug, "Found following AWS credentials in the task role: {}={}, {}={}, {}={}", | ||
| AWS_ACCESS_KEY_ID, access_key_id, AWS_SECRET_ACCESS_KEY, | ||
| secret_access_key.empty() ? "" : "*****", AWS_SESSION_TOKEN, | ||
| session_token.empty() ? "" : "*****"); | ||
|
|
||
| const auto expiration_str = document_json->getString(EXPIRATION, ""); | ||
| if (!expiration_str.empty()) { | ||
| absl::Time expiration_time; | ||
| if (absl::ParseTime(EXPIRATION_FORMAT, expiration_str, &expiration_time, nullptr)) { | ||
| ENVOY_LOG(debug, "Task role AWS credentials expiration time: {}", expiration_str); | ||
| expiration_time_ = absl::ToChronoTime(expiration_time); | ||
| } | ||
| } | ||
|
|
||
| last_updated_ = api_.timeSource().systemTime(); | ||
| cached_credentials_ = Credentials(access_key_id, secret_access_key, session_token); | ||
| } | ||
|
|
||
| Credentials CredentialsProviderChain::getCredentials() { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we have coverage for this function? https://239673-65214191-gh.circle-artifacts.com/0/coverage/coverage.source_extensions_filters_http_common_aws_credentials_provider_impl.cc.html
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks to catching that. Added tests. |
||
| for (auto& provider : providers_) { | ||
| const auto credentials = provider->getCredentials(); | ||
| if (credentials.accessKeyId() && credentials.secretAccessKey()) { | ||
| return credentials; | ||
| } | ||
| } | ||
|
|
||
| ENVOY_LOG(debug, "No AWS credentials found, using anonymous credentials"); | ||
| return Credentials(); | ||
| } | ||
|
|
||
| DefaultCredentialsProviderChain::DefaultCredentialsProviderChain( | ||
| Api::Api& api, const MetadataCredentialsProviderBase::MetadataFetcher& metadata_fetcher, | ||
| const CredentialsProviderChainFactories& factories) { | ||
| ENVOY_LOG(debug, "Using environment credentials provider"); | ||
| add(factories.createEnvironmentCredentialsProvider()); | ||
|
|
||
| const auto relative_uri = std::getenv(AWS_CONTAINER_CREDENTIALS_RELATIVE_URI); | ||
| const auto full_uri = std::getenv(AWS_CONTAINER_CREDENTIALS_FULL_URI); | ||
| const auto metadata_disabled = std::getenv(AWS_EC2_METADATA_DISABLED); | ||
|
|
||
| if (relative_uri != nullptr) { | ||
| const auto uri = std::string(CONTAINER_METADATA_HOST) + relative_uri; | ||
| ENVOY_LOG(debug, "Using task role credentials provider with URI: {}", uri); | ||
| add(factories.createTaskRoleCredentialsProvider(api, metadata_fetcher, uri)); | ||
| } else if (full_uri != nullptr) { | ||
| const auto authorization_token = std::getenv(AWS_CONTAINER_AUTHORIZATION_TOKEN); | ||
| if (authorization_token != nullptr) { | ||
| ENVOY_LOG(debug, | ||
| "Using task role credentials provider with URI: " | ||
| "{} and authorization token", | ||
| full_uri); | ||
| add(factories.createTaskRoleCredentialsProvider(api, metadata_fetcher, full_uri, | ||
| authorization_token)); | ||
| } else { | ||
| ENVOY_LOG(debug, "Using task role credentials provider with URI: {}", full_uri); | ||
| add(factories.createTaskRoleCredentialsProvider(api, metadata_fetcher, full_uri)); | ||
| } | ||
| } else if (metadata_disabled == nullptr || strncmp(metadata_disabled, TRUE, strlen(TRUE)) != 0) { | ||
| ENVOY_LOG(debug, "Using instance profile credentials provider"); | ||
| add(factories.createInstanceProfileCredentialsProvider(api, metadata_fetcher)); | ||
| } | ||
| } | ||
|
|
||
| } // namespace Aws | ||
| } // namespace Common | ||
| } // namespace HttpFilters | ||
| } // namespace Extensions | ||
| } // namespace Envoy | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I felt like this should be a struct but not feeling strongly about it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interface of this type is not public by default. Data members specifically are not public and can not be changed once set by the constructor. In that sense it feels more like a type (class) than a collection of data (struct).