Line data Source code
1 : #include "source/extensions/common/aws/credentials_provider_impl.h"
2 :
3 : #include <fstream>
4 :
5 : #include "envoy/common/exception.h"
6 :
7 : #include "source/common/common/lock_guard.h"
8 : #include "source/common/http/message_impl.h"
9 : #include "source/common/http/utility.h"
10 : #include "source/common/json/json_loader.h"
11 : #include "source/common/runtime/runtime_features.h"
12 : #include "source/common/tracing/http_tracer_impl.h"
13 : #include "source/extensions/common/aws/utility.h"
14 :
15 : #include "absl/strings/str_format.h"
16 : #include "absl/strings/str_split.h"
17 :
18 : namespace Envoy {
19 : namespace Extensions {
20 : namespace Common {
21 : namespace Aws {
22 :
23 : namespace {
24 :
25 : constexpr char AWS_ACCESS_KEY_ID[] = "AWS_ACCESS_KEY_ID";
26 : constexpr char AWS_SECRET_ACCESS_KEY[] = "AWS_SECRET_ACCESS_KEY";
27 : constexpr char AWS_SESSION_TOKEN[] = "AWS_SESSION_TOKEN";
28 : constexpr char AWS_ROLE_ARN[] = "AWS_ROLE_ARN";
29 : constexpr char AWS_WEB_IDENTITY_TOKEN_FILE[] = "AWS_WEB_IDENTITY_TOKEN_FILE";
30 : constexpr char AWS_ROLE_SESSION_NAME[] = "AWS_ROLE_SESSION_NAME";
31 :
32 : constexpr char CREDENTIALS[] = "Credentials";
33 : constexpr char ACCESS_KEY_ID[] = "AccessKeyId";
34 : constexpr char SECRET_ACCESS_KEY[] = "SecretAccessKey";
35 : constexpr char TOKEN[] = "Token";
36 : constexpr char EXPIRATION[] = "Expiration";
37 : constexpr char EXPIRATION_FORMAT[] = "%E4Y-%m-%dT%H:%M:%S%z";
38 : constexpr char TRUE[] = "true";
39 : constexpr char SESSION_TOKEN[] = "SessionToken";
40 : constexpr char WEB_IDENTITY_RESPONSE_ELEMENT[] = "AssumeRoleWithWebIdentityResponse";
41 : constexpr char WEB_IDENTITY_RESULT_ELEMENT[] = "AssumeRoleWithWebIdentityResult";
42 :
43 : constexpr char AWS_SHARED_CREDENTIALS_FILE[] = "AWS_SHARED_CREDENTIALS_FILE";
44 : constexpr char AWS_PROFILE[] = "AWS_PROFILE";
45 : constexpr char DEFAULT_AWS_SHARED_CREDENTIALS_FILE[] = "~/.aws/credentials";
46 : constexpr char DEFAULT_AWS_PROFILE[] = "default";
47 :
48 : constexpr char AWS_CONTAINER_CREDENTIALS_RELATIVE_URI[] = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI";
49 : constexpr char AWS_CONTAINER_CREDENTIALS_FULL_URI[] = "AWS_CONTAINER_CREDENTIALS_FULL_URI";
50 : constexpr char AWS_CONTAINER_AUTHORIZATION_TOKEN[] = "AWS_CONTAINER_AUTHORIZATION_TOKEN";
51 : constexpr char AWS_EC2_METADATA_DISABLED[] = "AWS_EC2_METADATA_DISABLED";
52 :
53 : constexpr std::chrono::hours REFRESH_INTERVAL{1};
54 : constexpr std::chrono::seconds REFRESH_GRACE_PERIOD{5};
55 : constexpr char EC2_METADATA_HOST[] = "169.254.169.254:80";
56 : constexpr char CONTAINER_METADATA_HOST[] = "169.254.170.2:80";
57 : constexpr char EC2_IMDS_TOKEN_RESOURCE[] = "/latest/api/token";
58 : constexpr char EC2_IMDS_TOKEN_HEADER[] = "X-aws-ec2-metadata-token";
59 : constexpr char EC2_IMDS_TOKEN_TTL_HEADER[] = "X-aws-ec2-metadata-token-ttl-seconds";
60 : constexpr char EC2_IMDS_TOKEN_TTL_DEFAULT_VALUE[] = "21600";
61 : constexpr char SECURITY_CREDENTIALS_PATH[] = "/latest/meta-data/iam/security-credentials";
62 :
63 : constexpr char EC2_METADATA_CLUSTER[] = "ec2_instance_metadata_server_internal";
64 : constexpr char CONTAINER_METADATA_CLUSTER[] = "ecs_task_metadata_server_internal";
65 : constexpr char STS_TOKEN_CLUSTER[] = "sts_token_service_internal";
66 :
67 : } // namespace
68 :
69 5 : Credentials EnvironmentCredentialsProvider::getCredentials() {
70 5 : ENVOY_LOG(debug, "Getting AWS credentials from the environment");
71 :
72 5 : const auto access_key_id = absl::NullSafeStringView(std::getenv(AWS_ACCESS_KEY_ID));
73 5 : if (access_key_id.empty()) {
74 5 : return Credentials();
75 5 : }
76 :
77 0 : const auto secret_access_key = absl::NullSafeStringView(std::getenv(AWS_SECRET_ACCESS_KEY));
78 0 : const auto session_token = absl::NullSafeStringView(std::getenv(AWS_SESSION_TOKEN));
79 :
80 0 : ENVOY_LOG(debug, "Found following AWS credentials in the environment: {}={}, {}={}, {}={}",
81 0 : AWS_ACCESS_KEY_ID, access_key_id, AWS_SECRET_ACCESS_KEY,
82 0 : secret_access_key.empty() ? "" : "*****", AWS_SESSION_TOKEN,
83 0 : session_token.empty() ? "" : "*****");
84 :
85 0 : return Credentials(access_key_id, secret_access_key, session_token);
86 5 : }
87 :
88 10 : void CachedCredentialsProviderBase::refreshIfNeeded() {
89 10 : const Thread::LockGuard lock(lock_);
90 10 : if (needsRefresh()) {
91 10 : refresh();
92 10 : }
93 10 : }
94 :
95 : // TODO(suniltheta): The field context is of type ServerFactoryContextOptRef so that an
96 : // optional empty value can be set. Especially in aws iam plugin the cluster manager
97 : // obtained from server factory context object is not fully initialized due to the
98 : // reasons explained in https://github.com/envoyproxy/envoy/issues/27586 which cannot
99 : // utilize http async client here to fetch AWS credentials. For time being if context
100 : // is empty then will use libcurl to fetch the credentials.
101 : MetadataCredentialsProviderBase::MetadataCredentialsProviderBase(
102 : Api::Api& api, ServerFactoryContextOptRef context,
103 : const CurlMetadataFetcher& fetch_metadata_using_curl,
104 : CreateMetadataFetcherCb create_metadata_fetcher_cb, absl::string_view cluster_name,
105 : const envoy::config::cluster::v3::Cluster::DiscoveryType cluster_type, absl::string_view uri)
106 : : api_(api), context_(context), fetch_metadata_using_curl_(fetch_metadata_using_curl),
107 : create_metadata_fetcher_cb_(create_metadata_fetcher_cb),
108 : cluster_name_(std::string(cluster_name)), cluster_type_(cluster_type), uri_(std::string(uri)),
109 : cache_duration_(getCacheDuration()),
110 6 : debug_name_(absl::StrCat("Fetching aws credentials from cluster=", cluster_name)) {
111 6 : if (context_) {
112 6 : context_->mainThreadDispatcher().post([this]() {
113 6 : if (!Utility::addInternalClusterStatic(context_->clusterManager(), cluster_name_,
114 6 : cluster_type_, uri_)) {
115 0 : ENVOY_LOG(critical,
116 0 : "Failed to add [STATIC cluster = {} with address = {}] or cluster not found",
117 0 : cluster_name_, uri_);
118 0 : return;
119 0 : }
120 6 : });
121 :
122 6 : tls_ = ThreadLocal::TypedSlot<ThreadLocalCredentialsCache>::makeUnique(context_->threadLocal());
123 6 : tls_->set(
124 6 : [](Envoy::Event::Dispatcher&) { return std::make_shared<ThreadLocalCredentialsCache>(); });
125 :
126 6 : cache_duration_timer_ = context_->mainThreadDispatcher().createTimer([this]() -> void {
127 0 : if (useHttpAsyncClient()) {
128 0 : const Thread::LockGuard lock(lock_);
129 0 : refresh();
130 0 : }
131 0 : });
132 :
133 6 : if (useHttpAsyncClient()) {
134 : // Register with init_manager, force the listener to wait for fetching (refresh).
135 0 : init_target_ =
136 0 : std::make_unique<Init::TargetImpl>(debug_name_, [this]() -> void { refresh(); });
137 0 : context_->initManager().add(*init_target_);
138 0 : }
139 6 : }
140 6 : }
141 :
142 5 : Credentials MetadataCredentialsProviderBase::getCredentials() {
143 5 : refreshIfNeeded();
144 5 : if (useHttpAsyncClient() && context_ && tls_) {
145 : // If server factor context was supplied then we would have thread local slot initialized.
146 0 : return *(*tls_)->credentials_.get();
147 5 : } else {
148 5 : return cached_credentials_;
149 5 : }
150 5 : }
151 :
152 6 : std::chrono::seconds MetadataCredentialsProviderBase::getCacheDuration() {
153 6 : return std::chrono::seconds(
154 6 : REFRESH_INTERVAL * 60 * 60 -
155 6 : REFRESH_GRACE_PERIOD /*TODO: Add jitter from context.api().randomGenerator()*/);
156 6 : }
157 :
158 0 : void MetadataCredentialsProviderBase::handleFetchDone() {
159 0 : if (useHttpAsyncClient() && context_) {
160 0 : if (init_target_) {
161 0 : init_target_->ready();
162 0 : init_target_.reset();
163 0 : }
164 0 : if (cache_duration_timer_ && !cache_duration_timer_->enabled()) {
165 0 : cache_duration_timer_->enableTimer(cache_duration_);
166 0 : }
167 0 : }
168 0 : }
169 :
170 : void MetadataCredentialsProviderBase::setCredentialsToAllThreads(
171 0 : CredentialsConstUniquePtr&& creds) {
172 0 : CredentialsConstSharedPtr shared_credentials = std::move(creds);
173 0 : if (tls_) {
174 0 : tls_->runOnAllThreads([shared_credentials](OptRef<ThreadLocalCredentialsCache> obj) {
175 0 : obj->credentials_ = shared_credentials;
176 0 : });
177 0 : }
178 0 : }
179 :
180 16 : bool MetadataCredentialsProviderBase::useHttpAsyncClient() {
181 16 : return Runtime::runtimeFeatureEnabled(
182 16 : "envoy.reloadable_features.use_http_client_to_fetch_aws_credentials");
183 16 : }
184 :
185 5 : bool CredentialsFileCredentialsProvider::needsRefresh() {
186 5 : return api_.timeSource().systemTime() - last_updated_ > REFRESH_INTERVAL;
187 5 : }
188 :
189 : std::string getEnvironmentVariableOrDefault(const std::string& variable_name,
190 10 : const std::string& default_value) {
191 10 : const char* value = getenv(variable_name.c_str());
192 10 : return (value != nullptr) && (value[0] != '\0') ? value : default_value;
193 10 : }
194 :
195 5 : void CredentialsFileCredentialsProvider::refresh() {
196 5 : ENVOY_LOG(debug, "Getting AWS credentials from the credentials file");
197 :
198 5 : const auto credentials_file = getEnvironmentVariableOrDefault(
199 5 : AWS_SHARED_CREDENTIALS_FILE, DEFAULT_AWS_SHARED_CREDENTIALS_FILE);
200 5 : const auto profile = getEnvironmentVariableOrDefault(AWS_PROFILE, DEFAULT_AWS_PROFILE);
201 :
202 5 : extractCredentials(credentials_file, profile);
203 5 : }
204 :
205 : void CredentialsFileCredentialsProvider::extractCredentials(const std::string& credentials_file,
206 5 : const std::string& profile) {
207 : // Update last_updated_ now so that even if this function returns before successfully
208 : // extracting credentials, this function won't be called again until after the REFRESH_INTERVAL.
209 : // This prevents envoy from attempting and failing to read the credentials file on every request
210 : // if there are errors extracting credentials from it (e.g. if the credentials file doesn't
211 : // exist).
212 5 : last_updated_ = api_.timeSource().systemTime();
213 :
214 5 : std::ifstream file(credentials_file);
215 5 : if (!file) {
216 5 : ENVOY_LOG(debug, "Error opening credentials file {}", credentials_file);
217 5 : return;
218 5 : }
219 :
220 0 : std::string access_key_id, secret_access_key, session_token;
221 0 : const auto profile_start = absl::StrFormat("[%s]", profile);
222 :
223 0 : bool found_profile = false;
224 0 : std::string line;
225 0 : while (std::getline(file, line)) {
226 0 : line = std::string(StringUtil::trim(line));
227 0 : if (line.empty()) {
228 0 : continue;
229 0 : }
230 :
231 0 : if (line == profile_start) {
232 0 : found_profile = true;
233 0 : continue;
234 0 : }
235 :
236 0 : if (found_profile) {
237 : // Stop reading once we find the start of the next profile.
238 0 : if (absl::StartsWith(line, "[")) {
239 0 : break;
240 0 : }
241 :
242 0 : std::vector<std::string> parts = absl::StrSplit(line, absl::MaxSplits('=', 1));
243 0 : if (parts.size() == 2) {
244 0 : const auto key = StringUtil::toUpper(StringUtil::trim(parts[0]));
245 0 : const auto val = StringUtil::trim(parts[1]);
246 0 : if (key == AWS_ACCESS_KEY_ID) {
247 0 : access_key_id = val;
248 0 : } else if (key == AWS_SECRET_ACCESS_KEY) {
249 0 : secret_access_key = val;
250 0 : } else if (key == AWS_SESSION_TOKEN) {
251 0 : session_token = val;
252 0 : }
253 0 : }
254 0 : }
255 0 : }
256 :
257 0 : ENVOY_LOG(debug, "Found following AWS credentials for profile '{}' in {}: {}={}, {}={}, {}={}",
258 0 : profile, credentials_file, AWS_ACCESS_KEY_ID, access_key_id, AWS_SECRET_ACCESS_KEY,
259 0 : secret_access_key.empty() ? "" : "*****", AWS_SESSION_TOKEN,
260 0 : session_token.empty() ? "" : "*****");
261 :
262 0 : cached_credentials_ = Credentials(access_key_id, secret_access_key, session_token);
263 0 : last_updated_ = api_.timeSource().systemTime();
264 0 : }
265 :
266 : InstanceProfileCredentialsProvider::InstanceProfileCredentialsProvider(
267 : Api::Api& api, ServerFactoryContextOptRef context,
268 : const CurlMetadataFetcher& fetch_metadata_using_curl,
269 : CreateMetadataFetcherCb create_metadata_fetcher_cb, absl::string_view cluster_name)
270 : : MetadataCredentialsProviderBase(
271 : api, context, fetch_metadata_using_curl, create_metadata_fetcher_cb, cluster_name,
272 6 : envoy::config::cluster::v3::Cluster::STATIC /*cluster_type*/, EC2_METADATA_HOST) {}
273 :
274 5 : bool InstanceProfileCredentialsProvider::needsRefresh() {
275 5 : return api_.timeSource().systemTime() - last_updated_ > REFRESH_INTERVAL;
276 5 : }
277 :
278 5 : void InstanceProfileCredentialsProvider::refresh() {
279 5 : ENVOY_LOG(debug, "Getting AWS credentials from the EC2MetadataService");
280 :
281 : // First request for a session TOKEN so that we can call EC2MetadataService securely.
282 : // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
283 5 : Http::RequestMessageImpl token_req_message;
284 5 : token_req_message.headers().setScheme(Http::Headers::get().SchemeValues.Http);
285 5 : token_req_message.headers().setMethod(Http::Headers::get().MethodValues.Put);
286 5 : token_req_message.headers().setHost(EC2_METADATA_HOST);
287 5 : token_req_message.headers().setPath(EC2_IMDS_TOKEN_RESOURCE);
288 5 : token_req_message.headers().setCopy(Http::LowerCaseString(EC2_IMDS_TOKEN_TTL_HEADER),
289 5 : EC2_IMDS_TOKEN_TTL_DEFAULT_VALUE);
290 :
291 5 : if (!useHttpAsyncClient() || !context_) {
292 : // Using curl to fetch the AWS credentials where we first get the token.
293 5 : const auto token_string = fetch_metadata_using_curl_(token_req_message);
294 5 : if (token_string) {
295 0 : ENVOY_LOG(debug, "Obtained token to make secure call to EC2MetadataService");
296 0 : fetchInstanceRole(std::move(token_string.value()));
297 5 : } else {
298 5 : ENVOY_LOG(warn,
299 5 : "Failed to get token from EC2MetadataService, falling back to less secure way");
300 5 : fetchInstanceRole(std::move(""));
301 5 : }
302 5 : } else {
303 : // Stop any existing timer.
304 0 : if (cache_duration_timer_ && cache_duration_timer_->enabled()) {
305 0 : cache_duration_timer_->disableTimer();
306 0 : }
307 : // Using Http async client to fetch the AWS credentials where we first get the token.
308 0 : if (!metadata_fetcher_) {
309 0 : metadata_fetcher_ = create_metadata_fetcher_cb_(context_->clusterManager(), clusterName());
310 0 : } else {
311 0 : metadata_fetcher_->cancel(); // Cancel if there is any inflight request.
312 0 : }
313 0 : on_async_fetch_cb_ = [this](const std::string&& arg) {
314 0 : return this->fetchInstanceRoleAsync(std::move(arg));
315 0 : };
316 0 : continue_on_async_fetch_failure_ = true;
317 0 : continue_on_async_fetch_failure_reason_ = "Token fetch failed so fall back to less secure way";
318 0 : metadata_fetcher_->fetch(token_req_message, Tracing::NullSpan::instance(), *this);
319 0 : }
320 5 : }
321 :
322 : void InstanceProfileCredentialsProvider::fetchInstanceRole(const std::string&& token_string,
323 5 : bool async /*default = false*/) {
324 : // Discover the Role of this instance.
325 5 : Http::RequestMessageImpl message;
326 5 : message.headers().setScheme(Http::Headers::get().SchemeValues.Http);
327 5 : message.headers().setMethod(Http::Headers::get().MethodValues.Get);
328 5 : message.headers().setHost(EC2_METADATA_HOST);
329 5 : message.headers().setPath(SECURITY_CREDENTIALS_PATH);
330 5 : if (!token_string.empty()) {
331 0 : message.headers().setCopy(Http::LowerCaseString(EC2_IMDS_TOKEN_HEADER),
332 0 : StringUtil::trim(token_string));
333 0 : }
334 :
335 5 : if (!async) {
336 : // Using curl to fetch the Instance Role.
337 5 : const auto instance_role_string = fetch_metadata_using_curl_(message);
338 5 : if (!instance_role_string) {
339 5 : ENVOY_LOG(error, "Could not retrieve credentials listing from the EC2MetadataService");
340 5 : return;
341 5 : }
342 0 : fetchCredentialFromInstanceRole(std::move(instance_role_string.value()),
343 0 : std::move(token_string));
344 0 : } else {
345 : // Using Http async client to fetch the Instance Role.
346 0 : metadata_fetcher_->cancel(); // Cancel if there is any inflight request.
347 0 : on_async_fetch_cb_ = [this, token_string = std::move(token_string)](const std::string&& arg) {
348 0 : return this->fetchCredentialFromInstanceRoleAsync(std::move(arg), std::move(token_string));
349 0 : };
350 0 : metadata_fetcher_->fetch(message, Tracing::NullSpan::instance(), *this);
351 0 : }
352 5 : }
353 :
354 : void InstanceProfileCredentialsProvider::fetchCredentialFromInstanceRole(
355 : const std::string&& instance_role, const std::string&& token_string,
356 0 : bool async /*default = false*/) {
357 :
358 0 : if (instance_role.empty()) {
359 0 : ENVOY_LOG(error, "No roles found to fetch AWS credentials from the EC2MetadataService");
360 0 : if (async) {
361 0 : handleFetchDone();
362 0 : }
363 0 : return;
364 0 : }
365 0 : const auto instance_role_list = StringUtil::splitToken(StringUtil::trim(instance_role), "\n");
366 0 : if (instance_role_list.empty()) {
367 0 : ENVOY_LOG(error, "No roles found to fetch AWS credentials from the EC2MetadataService");
368 0 : if (async) {
369 0 : handleFetchDone();
370 0 : }
371 0 : return;
372 0 : }
373 0 : ENVOY_LOG(debug, "AWS credentials list:\n{}", instance_role);
374 :
375 : // Only one Role can be associated with an instance:
376 : // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
377 0 : const auto credential_path =
378 0 : std::string(SECURITY_CREDENTIALS_PATH) + "/" +
379 0 : std::string(instance_role_list[0].data(), instance_role_list[0].size());
380 0 : ENVOY_LOG(debug, "AWS credentials path: {}", credential_path);
381 :
382 0 : Http::RequestMessageImpl message;
383 0 : message.headers().setScheme(Http::Headers::get().SchemeValues.Http);
384 0 : message.headers().setMethod(Http::Headers::get().MethodValues.Get);
385 0 : message.headers().setHost(EC2_METADATA_HOST);
386 0 : message.headers().setPath(credential_path);
387 0 : if (!token_string.empty()) {
388 0 : message.headers().setCopy(Http::LowerCaseString(EC2_IMDS_TOKEN_HEADER),
389 0 : StringUtil::trim(token_string));
390 0 : }
391 :
392 0 : if (!async) {
393 : // Fetch and parse the credentials.
394 0 : const auto credential_document = fetch_metadata_using_curl_(message);
395 0 : if (!credential_document) {
396 0 : ENVOY_LOG(error, "Could not load AWS credentials document from the EC2MetadataService");
397 0 : return;
398 0 : }
399 0 : extractCredentials(std::move(credential_document.value()));
400 0 : } else {
401 : // Using Http async client to fetch and parse the AWS credentials.
402 0 : metadata_fetcher_->cancel(); // Cancel if there is any inflight request.
403 0 : on_async_fetch_cb_ = [this](const std::string&& arg) {
404 0 : return this->extractCredentialsAsync(std::move(arg));
405 0 : };
406 0 : metadata_fetcher_->fetch(message, Tracing::NullSpan::instance(), *this);
407 0 : }
408 0 : }
409 :
410 : void InstanceProfileCredentialsProvider::extractCredentials(
411 0 : const std::string&& credential_document_value, bool async /*default = false*/) {
412 0 : if (credential_document_value.empty()) {
413 0 : if (async) {
414 0 : handleFetchDone();
415 0 : }
416 0 : return;
417 0 : }
418 0 : Json::ObjectSharedPtr document_json;
419 0 : TRY_NEEDS_AUDIT { document_json = Json::Factory::loadFromString(credential_document_value); }
420 0 : END_TRY catch (EnvoyException& e) {
421 0 : ENVOY_LOG(error, "Could not parse AWS credentials document: {}", e.what());
422 0 : if (async) {
423 0 : handleFetchDone();
424 0 : }
425 0 : return;
426 0 : }
427 :
428 0 : const auto access_key_id = document_json->getString(ACCESS_KEY_ID, "");
429 0 : const auto secret_access_key = document_json->getString(SECRET_ACCESS_KEY, "");
430 0 : const auto session_token = document_json->getString(TOKEN, "");
431 :
432 0 : ENVOY_LOG(debug,
433 0 : "Obtained following AWS credentials from the EC2MetadataService: {}={}, {}={}, {}={}",
434 0 : AWS_ACCESS_KEY_ID, access_key_id, AWS_SECRET_ACCESS_KEY,
435 0 : secret_access_key.empty() ? "" : "*****", AWS_SESSION_TOKEN,
436 0 : session_token.empty() ? "" : "*****");
437 :
438 0 : last_updated_ = api_.timeSource().systemTime();
439 0 : if (useHttpAsyncClient() && context_) {
440 0 : setCredentialsToAllThreads(
441 0 : std::make_unique<Credentials>(access_key_id, secret_access_key, session_token));
442 0 : } else {
443 0 : cached_credentials_ = Credentials(access_key_id, secret_access_key, session_token);
444 0 : }
445 0 : handleFetchDone();
446 0 : }
447 :
448 0 : void InstanceProfileCredentialsProvider::onMetadataSuccess(const std::string&& body) {
449 : // TODO(suniltheta): increment fetch success stats
450 0 : ENVOY_LOG(debug, "AWS Instance metadata fetch success, calling callback func");
451 0 : on_async_fetch_cb_(std::move(body));
452 0 : }
453 :
454 0 : void InstanceProfileCredentialsProvider::onMetadataError(Failure reason) {
455 : // TODO(suniltheta): increment fetch failed stats
456 0 : if (continue_on_async_fetch_failure_) {
457 0 : ENVOY_LOG(warn, "{}. Reason: {}", continue_on_async_fetch_failure_reason_,
458 0 : metadata_fetcher_->failureToString(reason));
459 0 : continue_on_async_fetch_failure_ = false;
460 0 : continue_on_async_fetch_failure_reason_ = "";
461 0 : on_async_fetch_cb_(std::move(""));
462 0 : } else {
463 0 : ENVOY_LOG(error, "AWS Instance metadata fetch failure: {}",
464 0 : metadata_fetcher_->failureToString(reason));
465 0 : handleFetchDone();
466 0 : }
467 0 : }
468 :
469 : TaskRoleCredentialsProvider::TaskRoleCredentialsProvider(
470 : Api::Api& api, ServerFactoryContextOptRef context,
471 : const CurlMetadataFetcher& fetch_metadata_using_curl,
472 : CreateMetadataFetcherCb create_metadata_fetcher_cb, absl::string_view credential_uri,
473 : absl::string_view authorization_token = {}, absl::string_view cluster_name = {})
474 : : MetadataCredentialsProviderBase(
475 : api, context, fetch_metadata_using_curl, create_metadata_fetcher_cb, cluster_name,
476 : envoy::config::cluster::v3::Cluster::STATIC /*cluster_type*/, credential_uri),
477 0 : credential_uri_(credential_uri), authorization_token_(authorization_token) {}
478 :
479 0 : bool TaskRoleCredentialsProvider::needsRefresh() {
480 0 : const auto now = api_.timeSource().systemTime();
481 0 : return (now - last_updated_ > REFRESH_INTERVAL) ||
482 0 : (expiration_time_ - now < REFRESH_GRACE_PERIOD);
483 0 : }
484 :
485 0 : void TaskRoleCredentialsProvider::refresh() {
486 0 : ENVOY_LOG(debug, "Getting AWS credentials from the task role at URI: {}", credential_uri_);
487 :
488 0 : absl::string_view host;
489 0 : absl::string_view path;
490 0 : Http::Utility::extractHostPathFromUri(credential_uri_, host, path);
491 :
492 0 : Http::RequestMessageImpl message;
493 0 : message.headers().setScheme(Http::Headers::get().SchemeValues.Http);
494 0 : message.headers().setMethod(Http::Headers::get().MethodValues.Get);
495 0 : message.headers().setHost(host);
496 0 : message.headers().setPath(path);
497 0 : message.headers().setCopy(Http::CustomHeaders::get().Authorization, authorization_token_);
498 0 : if (!useHttpAsyncClient() || !context_) {
499 : // Using curl to fetch the AWS credentials.
500 0 : const auto credential_document = fetch_metadata_using_curl_(message);
501 0 : if (!credential_document) {
502 0 : ENVOY_LOG(error, "Could not load AWS credentials document from the task role");
503 0 : return;
504 0 : }
505 0 : extractCredentials(std::move(credential_document.value()));
506 0 : } else {
507 : // Stop any existing timer.
508 0 : if (cache_duration_timer_ && cache_duration_timer_->enabled()) {
509 0 : cache_duration_timer_->disableTimer();
510 0 : }
511 : // Using Http async client to fetch the AWS credentials.
512 0 : if (!metadata_fetcher_) {
513 0 : metadata_fetcher_ = create_metadata_fetcher_cb_(context_->clusterManager(), clusterName());
514 0 : } else {
515 0 : metadata_fetcher_->cancel(); // Cancel if there is any inflight request.
516 0 : }
517 0 : on_async_fetch_cb_ = [this](const std::string&& arg) {
518 0 : return this->extractCredentials(std::move(arg));
519 0 : };
520 0 : metadata_fetcher_->fetch(message, Tracing::NullSpan::instance(), *this);
521 0 : }
522 0 : }
523 :
524 : void TaskRoleCredentialsProvider::extractCredentials(
525 0 : const std::string&& credential_document_value) {
526 0 : if (credential_document_value.empty()) {
527 0 : handleFetchDone();
528 0 : return;
529 0 : }
530 0 : Json::ObjectSharedPtr document_json;
531 0 : TRY_NEEDS_AUDIT { document_json = Json::Factory::loadFromString(credential_document_value); }
532 0 : END_TRY catch (EnvoyException& e) {
533 0 : ENVOY_LOG(error, "Could not parse AWS credentials document from the task role: {}", e.what());
534 0 : handleFetchDone();
535 0 : return;
536 0 : }
537 :
538 0 : const auto access_key_id = document_json->getString(ACCESS_KEY_ID, "");
539 0 : const auto secret_access_key = document_json->getString(SECRET_ACCESS_KEY, "");
540 0 : const auto session_token = document_json->getString(TOKEN, "");
541 :
542 0 : ENVOY_LOG(debug, "Found following AWS credentials in the task role: {}={}, {}={}, {}={}",
543 0 : AWS_ACCESS_KEY_ID, access_key_id, AWS_SECRET_ACCESS_KEY,
544 0 : secret_access_key.empty() ? "" : "*****", AWS_SESSION_TOKEN,
545 0 : session_token.empty() ? "" : "*****");
546 :
547 0 : const auto expiration_str = document_json->getString(EXPIRATION, "");
548 0 : if (!expiration_str.empty()) {
549 0 : absl::Time expiration_time;
550 0 : if (absl::ParseTime(EXPIRATION_FORMAT, expiration_str, &expiration_time, nullptr)) {
551 0 : ENVOY_LOG(debug, "Task role AWS credentials expiration time: {}", expiration_str);
552 0 : expiration_time_ = absl::ToChronoTime(expiration_time);
553 0 : }
554 0 : }
555 :
556 0 : last_updated_ = api_.timeSource().systemTime();
557 0 : if (useHttpAsyncClient() && context_) {
558 0 : setCredentialsToAllThreads(
559 0 : std::make_unique<Credentials>(access_key_id, secret_access_key, session_token));
560 0 : } else {
561 0 : cached_credentials_ = Credentials(access_key_id, secret_access_key, session_token);
562 0 : }
563 0 : handleFetchDone();
564 0 : }
565 :
566 0 : void TaskRoleCredentialsProvider::onMetadataSuccess(const std::string&& body) {
567 : // TODO(suniltheta): increment fetch success stats
568 0 : ENVOY_LOG(debug, "AWS Task metadata fetch success, calling callback func");
569 0 : on_async_fetch_cb_(std::move(body));
570 0 : }
571 :
572 0 : void TaskRoleCredentialsProvider::onMetadataError(Failure reason) {
573 : // TODO(suniltheta): increment fetch failed stats
574 0 : ENVOY_LOG(error, "AWS metadata fetch failure: {}", metadata_fetcher_->failureToString(reason));
575 0 : handleFetchDone();
576 0 : }
577 :
578 : WebIdentityCredentialsProvider::WebIdentityCredentialsProvider(
579 : Api::Api& api, ServerFactoryContextOptRef context,
580 : const CurlMetadataFetcher& fetch_metadata_using_curl,
581 : CreateMetadataFetcherCb create_metadata_fetcher_cb, absl::string_view token_file_path,
582 : absl::string_view sts_endpoint, absl::string_view role_arn, absl::string_view role_session_name,
583 : absl::string_view cluster_name = {})
584 : : MetadataCredentialsProviderBase(
585 : api, context, fetch_metadata_using_curl, create_metadata_fetcher_cb, cluster_name,
586 : envoy::config::cluster::v3::Cluster::LOGICAL_DNS /*cluster_type*/, sts_endpoint),
587 : token_file_path_(token_file_path), sts_endpoint_(sts_endpoint), role_arn_(role_arn),
588 0 : role_session_name_(role_session_name) {}
589 :
590 0 : bool WebIdentityCredentialsProvider::needsRefresh() {
591 0 : const auto now = api_.timeSource().systemTime();
592 0 : return (now - last_updated_ > REFRESH_INTERVAL) ||
593 0 : (expiration_time_ - now < REFRESH_GRACE_PERIOD);
594 0 : }
595 :
596 0 : void WebIdentityCredentialsProvider::refresh() {
597 : // If http async client is not enabled then just set empty credentials and return.
598 0 : if (!useHttpAsyncClient()) {
599 0 : cached_credentials_ = Credentials();
600 0 : return;
601 0 : }
602 :
603 0 : ENVOY_LOG(debug, "Getting AWS web identity credentials from STS: {}", sts_endpoint_);
604 :
605 0 : const auto web_token_file_or_error = api_.fileSystem().fileReadToEnd(token_file_path_);
606 0 : THROW_IF_STATUS_NOT_OK(web_token_file_or_error, throw);
607 0 : Http::RequestMessageImpl message;
608 0 : message.headers().setScheme(Http::Headers::get().SchemeValues.Https);
609 0 : message.headers().setMethod(Http::Headers::get().MethodValues.Get);
610 0 : message.headers().setHost(Http::Utility::parseAuthority(sts_endpoint_).host_);
611 0 : message.headers().setPath(
612 0 : fmt::format("/?Action=AssumeRoleWithWebIdentity"
613 0 : "&Version=2011-06-15"
614 0 : "&RoleSessionName={}"
615 0 : "&RoleArn={}"
616 0 : "&WebIdentityToken={}",
617 0 : Envoy::Http::Utility::PercentEncoding::encode(role_session_name_),
618 0 : Envoy::Http::Utility::PercentEncoding::encode(role_arn_),
619 0 : Envoy::Http::Utility::PercentEncoding::encode(web_token_file_or_error.value())));
620 : // Use the Accept header to ensure that AssumeRoleWithWebIdentityResponse is returned as JSON.
621 0 : message.headers().setReference(Http::CustomHeaders::get().Accept,
622 0 : Http::Headers::get().ContentTypeValues.Json);
623 : // Stop any existing timer.
624 0 : if (cache_duration_timer_ && cache_duration_timer_->enabled()) {
625 0 : cache_duration_timer_->disableTimer();
626 0 : }
627 : // Using Http async client to fetch the AWS credentials.
628 0 : if (!metadata_fetcher_) {
629 0 : metadata_fetcher_ = create_metadata_fetcher_cb_(context_->clusterManager(), clusterName());
630 0 : } else {
631 0 : metadata_fetcher_->cancel(); // Cancel if there is any inflight request.
632 0 : }
633 0 : on_async_fetch_cb_ = [this](const std::string&& arg) {
634 0 : return this->extractCredentials(std::move(arg));
635 0 : };
636 0 : metadata_fetcher_->fetch(message, Tracing::NullSpan::instance(), *this);
637 0 : }
638 :
639 : void WebIdentityCredentialsProvider::extractCredentials(
640 0 : const std::string&& credential_document_value) {
641 0 : if (credential_document_value.empty()) {
642 0 : handleFetchDone();
643 0 : ENVOY_LOG(error, "Could not load AWS credentials document from STS");
644 0 : return;
645 0 : }
646 :
647 0 : Json::ObjectSharedPtr document_json;
648 0 : TRY_NEEDS_AUDIT { document_json = Json::Factory::loadFromString(credential_document_value); }
649 0 : END_TRY catch (EnvoyException& e) {
650 0 : ENVOY_LOG(error, "Could not parse AWS credentials document from STS: {}", e.what());
651 0 : handleFetchDone();
652 0 : return;
653 0 : }
654 :
655 0 : absl::StatusOr<Json::ObjectSharedPtr> root_node =
656 0 : document_json->getObjectNoThrow(WEB_IDENTITY_RESPONSE_ELEMENT);
657 0 : if (!root_node.ok()) {
658 0 : ENVOY_LOG(error, "AWS STS credentials document is empty");
659 0 : handleFetchDone();
660 0 : return;
661 0 : }
662 0 : absl::StatusOr<Json::ObjectSharedPtr> result_node =
663 0 : root_node.value()->getObjectNoThrow(WEB_IDENTITY_RESULT_ELEMENT);
664 0 : if (!result_node.ok()) {
665 0 : ENVOY_LOG(error, "AWS STS returned an unexpected result");
666 0 : handleFetchDone();
667 0 : return;
668 0 : }
669 0 : absl::StatusOr<Json::ObjectSharedPtr> credentials =
670 0 : result_node.value()->getObjectNoThrow(CREDENTIALS);
671 0 : if (!credentials.ok()) {
672 0 : ENVOY_LOG(error, "AWS STS credentials document does not contain any credentials");
673 0 : handleFetchDone();
674 0 : return;
675 0 : }
676 :
677 0 : TRY_NEEDS_AUDIT {
678 0 : const auto access_key_id = credentials.value()->getString(ACCESS_KEY_ID, "");
679 0 : const auto secret_access_key = credentials.value()->getString(SECRET_ACCESS_KEY, "");
680 0 : const auto session_token = credentials.value()->getString(SESSION_TOKEN, "");
681 :
682 0 : ENVOY_LOG(debug, "Received the following AWS credentials from STS: {}={}, {}={}, {}={}",
683 0 : AWS_ACCESS_KEY_ID, access_key_id, AWS_SECRET_ACCESS_KEY,
684 0 : secret_access_key.empty() ? "" : "*****", AWS_SESSION_TOKEN,
685 0 : session_token.empty() ? "" : "*****");
686 0 : setCredentialsToAllThreads(
687 0 : std::make_unique<Credentials>(access_key_id, secret_access_key, session_token));
688 0 : }
689 0 : END_TRY catch (EnvoyException& e) {
690 0 : ENVOY_LOG(error, "Bad format, could not parse AWS credentials document from STS: {}", e.what());
691 0 : handleFetchDone();
692 0 : return;
693 0 : }
694 :
695 0 : TRY_NEEDS_AUDIT {
696 0 : const auto expiration = credentials.value()->getInteger(EXPIRATION, 0);
697 0 : if (expiration != 0) {
698 0 : expiration_time_ =
699 0 : std::chrono::time_point<std::chrono::system_clock>(std::chrono::seconds(expiration));
700 0 : ENVOY_LOG(debug, "AWS STS credentials expiration time (unix timestamp): {}", expiration);
701 0 : } else {
702 0 : expiration_time_ = api_.timeSource().systemTime() + REFRESH_INTERVAL;
703 0 : ENVOY_LOG(warn, "Could not get Expiration value of AWS credentials document from STS, so "
704 0 : "setting expiration to 1 hour in future");
705 0 : }
706 0 : }
707 0 : END_TRY catch (EnvoyException& e) {
708 0 : expiration_time_ = api_.timeSource().systemTime() + REFRESH_INTERVAL;
709 0 : ENVOY_LOG(warn,
710 0 : "Could not parse Expiration value of AWS credentials document from STS: {}, so "
711 0 : "setting expiration to 1 hour in future",
712 0 : e.what());
713 0 : }
714 :
715 0 : last_updated_ = api_.timeSource().systemTime();
716 0 : handleFetchDone();
717 0 : }
718 :
719 0 : void WebIdentityCredentialsProvider::onMetadataSuccess(const std::string&& body) {
720 : // TODO(suniltheta): increment fetch success stats
721 0 : ENVOY_LOG(debug, "AWS metadata fetch from STS success, calling callback func");
722 0 : on_async_fetch_cb_(std::move(body));
723 0 : }
724 :
725 0 : void WebIdentityCredentialsProvider::onMetadataError(Failure reason) {
726 : // TODO(suniltheta): increment fetch failed stats
727 0 : ENVOY_LOG(error, "AWS metadata fetch failure: {}", metadata_fetcher_->failureToString(reason));
728 0 : handleFetchDone();
729 0 : }
730 :
731 5 : Credentials CredentialsProviderChain::getCredentials() {
732 15 : for (auto& provider : providers_) {
733 15 : const auto credentials = provider->getCredentials();
734 15 : if (credentials.accessKeyId() && credentials.secretAccessKey()) {
735 0 : return credentials;
736 0 : }
737 15 : }
738 :
739 5 : ENVOY_LOG(debug, "No AWS credentials found, using anonymous credentials");
740 5 : return Credentials();
741 5 : }
742 :
743 : DefaultCredentialsProviderChain::DefaultCredentialsProviderChain(
744 : Api::Api& api, ServerFactoryContextOptRef context, absl::string_view region,
745 : const MetadataCredentialsProviderBase::CurlMetadataFetcher& fetch_metadata_using_curl,
746 6 : const CredentialsProviderChainFactories& factories) {
747 6 : ENVOY_LOG(debug, "Using environment credentials provider");
748 6 : add(factories.createEnvironmentCredentialsProvider());
749 :
750 6 : if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.enable_aws_credentials_file")) {
751 6 : ENVOY_LOG(debug, "Using credentials file credentials provider");
752 6 : add(factories.createCredentialsFileCredentialsProvider(api));
753 6 : } else {
754 0 : ENVOY_LOG(debug, "Not using credential file credentials provider because it is not enabled");
755 0 : }
756 :
757 : // WebIdentityCredentialsProvider can be used only if `context` is supplied which is required to
758 : // use http async http client to make http calls to fetch the credentials.
759 6 : if (context) {
760 6 : const auto web_token_path = absl::NullSafeStringView(std::getenv(AWS_WEB_IDENTITY_TOKEN_FILE));
761 6 : const auto role_arn = absl::NullSafeStringView(std::getenv(AWS_ROLE_ARN));
762 6 : if (!web_token_path.empty() && !role_arn.empty()) {
763 0 : const auto role_session_name = absl::NullSafeStringView(std::getenv(AWS_ROLE_SESSION_NAME));
764 0 : std::string actual_session_name;
765 0 : if (!role_session_name.empty()) {
766 0 : actual_session_name = std::string(role_session_name);
767 0 : } else {
768 : // In practice, this value will be provided by the environment, so the placeholder value is
769 : // not important. Some AWS SDKs use time in nanoseconds, so we'll just use that.
770 0 : const auto now_nanos = std::chrono::duration_cast<std::chrono::nanoseconds>(
771 0 : api.timeSource().systemTime().time_since_epoch())
772 0 : .count();
773 0 : actual_session_name = fmt::format("{}", now_nanos);
774 0 : }
775 0 : const auto sts_endpoint = Utility::getSTSEndpoint(region) + ":443";
776 0 : ENVOY_LOG(
777 0 : debug,
778 0 : "Using web identity credentials provider with STS endpoint: {} and session name: {}",
779 0 : sts_endpoint, actual_session_name);
780 0 : add(factories.createWebIdentityCredentialsProvider(
781 0 : api, context, fetch_metadata_using_curl, MetadataFetcher::create, STS_TOKEN_CLUSTER,
782 0 : web_token_path, sts_endpoint, role_arn, actual_session_name));
783 0 : }
784 6 : }
785 :
786 : // Even if WebIdentity is supported keep the fallback option open so that
787 : // Envoy can use other credentials provider if available.
788 6 : const auto relative_uri =
789 6 : absl::NullSafeStringView(std::getenv(AWS_CONTAINER_CREDENTIALS_RELATIVE_URI));
790 6 : const auto full_uri = absl::NullSafeStringView(std::getenv(AWS_CONTAINER_CREDENTIALS_FULL_URI));
791 6 : const auto metadata_disabled = absl::NullSafeStringView(std::getenv(AWS_EC2_METADATA_DISABLED));
792 :
793 6 : if (!relative_uri.empty()) {
794 0 : const auto uri = absl::StrCat(CONTAINER_METADATA_HOST, relative_uri);
795 0 : ENVOY_LOG(debug, "Using task role credentials provider with URI: {}", uri);
796 0 : add(factories.createTaskRoleCredentialsProvider(api, context, fetch_metadata_using_curl,
797 0 : MetadataFetcher::create,
798 0 : CONTAINER_METADATA_CLUSTER, uri));
799 6 : } else if (!full_uri.empty()) {
800 0 : const auto authorization_token =
801 0 : absl::NullSafeStringView(std::getenv(AWS_CONTAINER_AUTHORIZATION_TOKEN));
802 0 : if (!authorization_token.empty()) {
803 0 : ENVOY_LOG(debug,
804 0 : "Using task role credentials provider with URI: "
805 0 : "{} and authorization token",
806 0 : full_uri);
807 0 : add(factories.createTaskRoleCredentialsProvider(
808 0 : api, context, fetch_metadata_using_curl, MetadataFetcher::create,
809 0 : CONTAINER_METADATA_CLUSTER, full_uri, authorization_token));
810 0 : } else {
811 0 : ENVOY_LOG(debug, "Using task role credentials provider with URI: {}", full_uri);
812 0 : add(factories.createTaskRoleCredentialsProvider(api, context, fetch_metadata_using_curl,
813 0 : MetadataFetcher::create,
814 0 : CONTAINER_METADATA_CLUSTER, full_uri));
815 0 : }
816 6 : } else if (metadata_disabled != TRUE) {
817 6 : ENVOY_LOG(debug, "Using instance profile credentials provider");
818 6 : add(factories.createInstanceProfileCredentialsProvider(
819 6 : api, context, fetch_metadata_using_curl, MetadataFetcher::create, EC2_METADATA_CLUSTER));
820 6 : }
821 6 : }
822 :
823 : } // namespace Aws
824 : } // namespace Common
825 : } // namespace Extensions
826 : } // namespace Envoy
|