Skip to content
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
15 changes: 15 additions & 0 deletions source/extensions/filters/http/common/aws/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,21 @@ envoy_cc_library(
external_deps = ["abseil_optional"],
)

envoy_cc_library(
name = "credentials_provider_impl_lib",
srcs = ["credentials_provider_impl.cc"],
hdrs = ["credentials_provider_impl.h"],
external_deps = ["abseil_time"],
deps = [
":credentials_provider_interface",
"//include/envoy/api:api_interface",
"//source/common/common:logger_lib",
"//source/common/common:thread_lib",
"//source/common/http:utility_lib",
"//source/common/json:json_loader_lib",
],
)

envoy_cc_library(
name = "utility_lib",
srcs = ["utility.cc"],
Expand Down
43 changes: 33 additions & 10 deletions source/extensions/filters/http/common/aws/credentials_provider.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Copy Markdown
Member

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.

Copy link
Copy Markdown
Member Author

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).

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(),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defaulting to absl::string_view() seems unnecessary? For convenience you always have the default constructor Credentials()?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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 Credentials() = default. To me it looks simpler to have a single constructor, because relying on the default one would not lead to code reduction.

absl::string_view secret_access_key = absl::string_view(),
absl::string_view session_token = absl::string_view()) {
if (!access_key_id.empty()) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean we always need access_key_id? and we require either secret_access_key or session_token to be there? This if statements seem not necessary. What is the implication if we don't have (or empty) access_key_id?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Credentials can be one of:

  • Empty
  • Have access key only
  • Have access key and secret key
  • Have access key, secret key and session token

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;
};

Expand All @@ -50,4 +73,4 @@ using CredentialsProviderSharedPtr = std::shared_ptr<CredentialsProvider>;
} // namespace Common
} // namespace HttpFilters
} // namespace Extensions
} // namespace Envoy
} // namespace Envoy
220 changes: 220 additions & 0 deletions source/extensions/filters/http/common/aws/credentials_provider_impl.cc
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() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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
Loading