1use super::client::error::ImdsError;
12use crate::imds::{self, Client};
13use crate::json_credentials::{parse_json_credentials, JsonCredentials, RefreshableCredentials};
14use crate::provider_config::ProviderConfig;
15use aws_credential_types::credential_feature::AwsCredentialFeature;
16use aws_credential_types::provider::{self, error::CredentialsError, future, ProvideCredentials};
17use aws_credential_types::Credentials;
18use aws_smithy_async::time::SharedTimeSource;
19use aws_types::os_shim_internal::Env;
20use std::borrow::Cow;
21use std::error::Error as StdError;
22use std::fmt;
23use std::sync::{Arc, RwLock};
24use std::time::{Duration, SystemTime};
25
26const CREDENTIAL_EXPIRATION_INTERVAL: Duration = Duration::from_secs(10 * 60);
27const WARNING_FOR_EXTENDING_CREDENTIALS_EXPIRY: &str =
28 "Attempting credential expiration extension due to a credential service availability issue. \
29 A refresh of these credentials will be attempted again within the next";
30
31#[derive(Debug)]
32struct ImdsCommunicationError {
33 source: Box<dyn StdError + Send + Sync + 'static>,
34}
35
36impl fmt::Display for ImdsCommunicationError {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 write!(f, "could not communicate with IMDS")
39 }
40}
41
42impl StdError for ImdsCommunicationError {
43 fn source(&self) -> Option<&(dyn StdError + 'static)> {
44 Some(self.source.as_ref())
45 }
46}
47
48#[derive(Debug)]
52pub struct ImdsCredentialsProvider {
53 client: Client,
54 env: Env,
55 profile: Option<String>,
56 time_source: SharedTimeSource,
57 last_retrieved_credentials: Arc<RwLock<Option<Credentials>>>,
58}
59
60#[derive(Default, Debug)]
62pub struct Builder {
63 provider_config: Option<ProviderConfig>,
64 profile_override: Option<String>,
65 imds_override: Option<imds::Client>,
66 last_retrieved_credentials: Option<Credentials>,
67}
68
69impl Builder {
70 pub fn configure(mut self, provider_config: &ProviderConfig) -> Self {
72 self.provider_config = Some(provider_config.clone());
73 self
74 }
75
76 pub fn profile(mut self, profile: impl Into<String>) -> Self {
85 self.profile_override = Some(profile.into());
86 self
87 }
88
89 pub fn imds_client(mut self, client: imds::Client) -> Self {
96 self.imds_override = Some(client);
97 self
98 }
99
100 #[allow(dead_code)]
101 #[cfg(test)]
102 fn last_retrieved_credentials(mut self, credentials: Credentials) -> Self {
103 self.last_retrieved_credentials = Some(credentials);
104 self
105 }
106
107 pub fn build(self) -> ImdsCredentialsProvider {
109 let provider_config = self.provider_config.unwrap_or_default();
110 let env = provider_config.env();
111 let client = self
112 .imds_override
113 .unwrap_or_else(|| imds::Client::builder().configure(&provider_config).build());
114 ImdsCredentialsProvider {
115 client,
116 env,
117 profile: self.profile_override,
118 time_source: provider_config.time_source(),
119 last_retrieved_credentials: Arc::new(RwLock::new(self.last_retrieved_credentials)),
120 }
121 }
122}
123
124mod codes {
125 pub(super) const ASSUME_ROLE_UNAUTHORIZED_ACCESS: &str = "AssumeRoleUnauthorizedAccess";
126}
127
128impl ProvideCredentials for ImdsCredentialsProvider {
129 fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a>
130 where
131 Self: 'a,
132 {
133 future::ProvideCredentials::new(self.credentials())
134 }
135
136 fn fallback_on_interrupt(&self) -> Option<Credentials> {
137 self.last_retrieved_credentials.read().unwrap().clone()
138 }
139}
140
141impl ImdsCredentialsProvider {
142 pub fn builder() -> Builder {
144 Builder::default()
145 }
146
147 fn imds_disabled(&self) -> bool {
148 match self.env.get(super::env::EC2_METADATA_DISABLED) {
149 Ok(value) => value.eq_ignore_ascii_case("true"),
150 _ => false,
151 }
152 }
153
154 async fn get_profile_uncached(&self) -> Result<String, CredentialsError> {
156 match self
157 .client
158 .get("/latest/meta-data/iam/security-credentials/")
159 .await
160 {
161 Ok(profile) => Ok(profile.as_ref().into()),
162 Err(ImdsError::ErrorResponse(context))
163 if context.response().status().as_u16() == 404 =>
164 {
165 tracing::warn!(
166 "received 404 from IMDS when loading profile information. \
167 Hint: This instance may not have an IAM role associated."
168 );
169 Err(CredentialsError::not_loaded("received 404 from IMDS"))
170 }
171 Err(ImdsError::FailedToLoadToken(context)) if context.is_dispatch_failure() => {
172 Err(CredentialsError::not_loaded(ImdsCommunicationError {
173 source: context.into_source().into(),
174 }))
175 }
176 Err(other) => Err(CredentialsError::provider_error(other)),
177 }
178 }
179
180 fn maybe_extend_expiration(&self, expiration: SystemTime) -> SystemTime {
184 let now = self.time_source.now();
185 if now < expiration {
187 return expiration;
188 }
189
190 let mut rng = fastrand::Rng::with_seed(
191 now.duration_since(SystemTime::UNIX_EPOCH)
192 .expect("now should be after UNIX EPOCH")
193 .as_secs(),
194 );
195 let refresh_offset = CREDENTIAL_EXPIRATION_INTERVAL + Duration::from_secs(rng.u64(0..=300));
200 let new_expiry = now + refresh_offset;
201
202 tracing::warn!(
203 "{WARNING_FOR_EXTENDING_CREDENTIALS_EXPIRY} {:.2} minutes.",
204 refresh_offset.as_secs_f64() / 60.0,
205 );
206
207 new_expiry
208 }
209
210 async fn retrieve_credentials(&self) -> provider::Result {
211 if self.imds_disabled() {
212 let err = format!(
213 "IMDS disabled by {} env var set to `true`",
214 super::env::EC2_METADATA_DISABLED
215 );
216 tracing::debug!(err);
217 return Err(CredentialsError::not_loaded(err));
218 }
219 tracing::debug!("loading credentials from IMDS");
220 let profile: Cow<'_, str> = match &self.profile {
221 Some(profile) => profile.into(),
222 None => self.get_profile_uncached().await?.into(),
223 };
224 tracing::debug!(profile = %profile, "loaded profile");
225 let credentials = self
226 .client
227 .get(format!(
228 "/latest/meta-data/iam/security-credentials/{}",
229 profile
230 ))
231 .await
232 .map_err(CredentialsError::provider_error)?;
233 match parse_json_credentials(credentials.as_ref()) {
234 Ok(JsonCredentials::RefreshableCredentials(RefreshableCredentials {
235 access_key_id,
236 secret_access_key,
237 session_token,
238 account_id,
239 expiration,
240 ..
241 })) => {
242 let _ = account_id;
244 let expiration = self.maybe_extend_expiration(expiration);
245 let creds = Credentials::new(
246 access_key_id,
247 secret_access_key,
248 Some(session_token.to_string()),
249 expiration.into(),
250 "IMDSv2",
251 );
252 *self.last_retrieved_credentials.write().unwrap() = Some(creds.clone());
253 Ok(creds)
254 }
255 Ok(JsonCredentials::Error { code, message })
256 if code == codes::ASSUME_ROLE_UNAUTHORIZED_ACCESS =>
257 {
258 Err(CredentialsError::invalid_configuration(format!(
259 "Incorrect IMDS/IAM configuration: [{}] {}. \
260 Hint: Does this role have a trust relationship with EC2?",
261 code, message
262 )))
263 }
264 Ok(JsonCredentials::Error { code, message }) => {
265 Err(CredentialsError::provider_error(format!(
266 "Error retrieving credentials from IMDS: {} {}",
267 code, message
268 )))
269 }
270 Err(invalid) => Err(CredentialsError::unhandled(invalid)),
272 }
273 .map(|mut creds| {
274 creds
275 .get_property_mut_or_default::<Vec<AwsCredentialFeature>>()
276 .push(AwsCredentialFeature::CredentialsImds);
277 creds
278 })
279 }
280
281 async fn credentials(&self) -> provider::Result {
282 match self.retrieve_credentials().await {
283 creds @ Ok(_) => creds,
284 err => match &*self.last_retrieved_credentials.read().unwrap() {
286 Some(creds) => Ok(creds.clone()),
287 _ => err,
288 },
289 }
290 }
291}
292
293#[cfg(test)]
294mod test {
295 use super::*;
296 use crate::imds::client::test::{
297 imds_request, imds_response, make_imds_client, token_request, token_response,
298 };
299 use crate::provider_config::ProviderConfig;
300 use aws_credential_types::credential_feature::AwsCredentialFeature;
301 use aws_credential_types::provider::ProvideCredentials;
302 use aws_smithy_async::test_util::instant_time_and_sleep;
303 use aws_smithy_http_client::test_util::{ReplayEvent, StaticReplayClient};
304 use aws_smithy_types::body::SdkBody;
305 use std::time::{Duration, UNIX_EPOCH};
306 use tracing_test::traced_test;
307
308 const TOKEN_A: &str = "token_a";
309
310 #[tokio::test]
311 async fn profile_is_not_cached() {
312 let http_client = StaticReplayClient::new(vec![
313 ReplayEvent::new(
314 token_request("http://169.254.169.254", 21600),
315 token_response(21600, TOKEN_A),
316 ),
317 ReplayEvent::new(
318 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
319 imds_response(r#"profile-name"#),
320 ),
321 ReplayEvent::new(
322 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name", TOKEN_A),
323 imds_response("{\n \"Code\" : \"Success\",\n \"LastUpdated\" : \"2021-09-20T21:42:26Z\",\n \"Type\" : \"AWS-HMAC\",\n \"AccessKeyId\" : \"ASIARTEST\",\n \"SecretAccessKey\" : \"testsecret\",\n \"Token\" : \"testtoken\",\n \"Expiration\" : \"2021-09-21T04:16:53Z\"\n}"),
324 ),
325 ReplayEvent::new(
326 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
327 imds_response(r#"different-profile"#),
328 ),
329 ReplayEvent::new(
330 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/different-profile", TOKEN_A),
331 imds_response("{\n \"Code\" : \"Success\",\n \"LastUpdated\" : \"2021-09-20T21:42:26Z\",\n \"Type\" : \"AWS-HMAC\",\n \"AccessKeyId\" : \"ASIARTEST2\",\n \"SecretAccessKey\" : \"testsecret\",\n \"Token\" : \"testtoken\",\n \"Expiration\" : \"2021-09-21T04:16:53Z\"\n}"),
332 ),
333 ]);
334 let client = ImdsCredentialsProvider::builder()
335 .imds_client(make_imds_client(&http_client))
336 .configure(&ProviderConfig::no_configuration())
337 .build();
338 let creds1 = client.provide_credentials().await.expect("valid creds");
339 let creds2 = client.provide_credentials().await.expect("valid creds");
340 assert_eq!(creds1.access_key_id(), "ASIARTEST");
341 assert_eq!(creds2.access_key_id(), "ASIARTEST2");
342 http_client.assert_requests_match(&[]);
343 }
344
345 #[tokio::test]
346 #[traced_test]
347 async fn credentials_not_stale_should_be_used_as_they_are() {
348 let http_client = StaticReplayClient::new(vec![
349 ReplayEvent::new(
350 token_request("http://169.254.169.254", 21600),
351 token_response(21600, TOKEN_A),
352 ),
353 ReplayEvent::new(
354 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
355 imds_response(r#"profile-name"#),
356 ),
357 ReplayEvent::new(
358 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name", TOKEN_A),
359 imds_response("{\n \"Code\" : \"Success\",\n \"LastUpdated\" : \"2021-09-20T21:42:26Z\",\n \"Type\" : \"AWS-HMAC\",\n \"AccessKeyId\" : \"ASIARTEST\",\n \"SecretAccessKey\" : \"testsecret\",\n \"Token\" : \"testtoken\",\n \"Expiration\" : \"2021-09-21T04:16:53Z\"\n}"),
360 ),
361 ]);
362
363 let time_of_request_to_fetch_credentials = UNIX_EPOCH + Duration::from_secs(1632197810);
366 let (time_source, sleep) = instant_time_and_sleep(time_of_request_to_fetch_credentials);
367
368 let provider_config = ProviderConfig::no_configuration()
369 .with_http_client(http_client.clone())
370 .with_sleep_impl(sleep)
371 .with_time_source(time_source);
372 let client = crate::imds::Client::builder()
373 .configure(&provider_config)
374 .build();
375 let provider = ImdsCredentialsProvider::builder()
376 .configure(&provider_config)
377 .imds_client(client)
378 .build();
379 let creds = provider.provide_credentials().await.expect("valid creds");
380 assert_eq!(
382 creds.expiry(),
383 UNIX_EPOCH.checked_add(Duration::from_secs(1632197813))
384 );
385 http_client.assert_requests_match(&[]);
386
387 assert!(!logs_contain(WARNING_FOR_EXTENDING_CREDENTIALS_EXPIRY));
389 }
390 #[tokio::test]
391 #[traced_test]
392 async fn expired_credentials_should_be_extended() {
393 let http_client = StaticReplayClient::new(vec![
394 ReplayEvent::new(
395 token_request("http://169.254.169.254", 21600),
396 token_response(21600, TOKEN_A),
397 ),
398 ReplayEvent::new(
399 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
400 imds_response(r#"profile-name"#),
401 ),
402 ReplayEvent::new(
403 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name", TOKEN_A),
404 imds_response("{\n \"Code\" : \"Success\",\n \"LastUpdated\" : \"2021-09-20T21:42:26Z\",\n \"Type\" : \"AWS-HMAC\",\n \"AccessKeyId\" : \"ASIARTEST\",\n \"SecretAccessKey\" : \"testsecret\",\n \"Token\" : \"testtoken\",\n \"Expiration\" : \"2021-09-21T04:16:53Z\"\n}"),
405 ),
406 ]);
407
408 let time_of_request_to_fetch_credentials = UNIX_EPOCH + Duration::from_secs(1632246085);
410 let (time_source, sleep) = instant_time_and_sleep(time_of_request_to_fetch_credentials);
411
412 let provider_config = ProviderConfig::no_configuration()
413 .with_http_client(http_client.clone())
414 .with_sleep_impl(sleep)
415 .with_time_source(time_source);
416 let client = crate::imds::Client::builder()
417 .configure(&provider_config)
418 .build();
419 let provider = ImdsCredentialsProvider::builder()
420 .configure(&provider_config)
421 .imds_client(client)
422 .build();
423 let creds = provider.provide_credentials().await.expect("valid creds");
424 assert!(creds.expiry().unwrap() > time_of_request_to_fetch_credentials);
425 http_client.assert_requests_match(&[]);
426
427 assert!(logs_contain(WARNING_FOR_EXTENDING_CREDENTIALS_EXPIRY));
429 }
430
431 #[tokio::test]
432 #[cfg(feature = "default-https-client")]
433 async fn read_timeout_during_credentials_refresh_should_yield_last_retrieved_credentials() {
434 let client = crate::imds::Client::builder()
435 .endpoint("http://240.0.0.0")
437 .unwrap()
438 .build();
439 let expected = aws_credential_types::Credentials::for_tests();
440 let provider = ImdsCredentialsProvider::builder()
441 .imds_client(client)
442 .last_retrieved_credentials(expected.clone())
444 .build();
445 let actual = provider.provide_credentials().await;
446 assert_eq!(actual.unwrap(), expected);
447 }
448
449 #[tokio::test]
450 #[cfg(feature = "default-https-client")]
451 async fn read_timeout_during_credentials_refresh_should_error_without_last_retrieved_credentials(
452 ) {
453 let client = crate::imds::Client::builder()
454 .endpoint("http://240.0.0.0")
456 .unwrap()
457 .build();
458 let provider = ImdsCredentialsProvider::builder()
459 .imds_client(client)
460 .build();
462 let actual = provider.provide_credentials().await;
463 assert!(
464 matches!(actual, Err(CredentialsError::CredentialsNotLoaded(_))),
465 "\nexpected: Err(CredentialsError::CredentialsNotLoaded(_))\nactual: {actual:?}"
466 );
467 }
468
469 #[cfg_attr(windows, ignore)]
471 #[tokio::test]
472 #[cfg(feature = "default-https-client")]
473 async fn external_timeout_during_credentials_refresh_should_yield_last_retrieved_credentials() {
474 use aws_smithy_async::rt::sleep::AsyncSleep;
475 let client = crate::imds::Client::builder()
476 .endpoint("http://240.0.0.0")
478 .unwrap()
479 .build();
480 let expected = aws_credential_types::Credentials::for_tests();
481 let provider = ImdsCredentialsProvider::builder()
482 .imds_client(client)
483 .configure(&ProviderConfig::no_configuration())
484 .last_retrieved_credentials(expected.clone())
486 .build();
487 let sleeper = aws_smithy_async::rt::sleep::TokioSleep::new();
488 let timeout = aws_smithy_async::future::timeout::Timeout::new(
489 provider.provide_credentials(),
490 sleeper.sleep(std::time::Duration::from_millis(100)),
492 );
493 match timeout.await {
494 Ok(_) => panic!("provide_credentials completed before timeout future"),
495 Err(_err) => match provider.fallback_on_interrupt() {
496 Some(actual) => assert_eq!(actual, expected),
497 None => panic!(
498 "provide_credentials timed out and no credentials returned from fallback_on_interrupt"
499 ),
500 },
501 };
502 }
503
504 #[tokio::test]
505 async fn fallback_credentials_should_be_used_when_imds_returns_500_during_credentials_refresh()
506 {
507 let http_client = StaticReplayClient::new(vec![
508 ReplayEvent::new(
511 token_request("http://169.254.169.254", 21600),
512 token_response(21600, TOKEN_A),
513 ),
514 ReplayEvent::new(
515 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
516 imds_response(r#"profile-name"#),
517 ),
518 ReplayEvent::new(
519 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name", TOKEN_A),
520 imds_response("{\n \"Code\" : \"Success\",\n \"LastUpdated\" : \"2021-09-20T21:42:26Z\",\n \"Type\" : \"AWS-HMAC\",\n \"AccessKeyId\" : \"ASIARTEST\",\n \"SecretAccessKey\" : \"testsecret\",\n \"Token\" : \"testtoken\",\n \"Expiration\" : \"2021-09-21T04:16:53Z\"\n}"),
521 ),
522 ReplayEvent::new(
525 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
526 http::Response::builder().status(500).body(SdkBody::empty()).unwrap(),
527 ),
528 ]);
529 let provider = ImdsCredentialsProvider::builder()
530 .imds_client(make_imds_client(&http_client))
531 .configure(&ProviderConfig::no_configuration())
532 .build();
533 let creds1 = provider.provide_credentials().await.expect("valid creds");
534 assert_eq!(creds1.access_key_id(), "ASIARTEST");
535 let creds2 = provider.provide_credentials().await.expect("valid creds");
537 assert_eq!(creds1, creds2);
538 http_client.assert_requests_match(&[]);
539 }
540
541 #[tokio::test]
542 async fn credentials_feature() {
543 let http_client = StaticReplayClient::new(vec![
544 ReplayEvent::new(
545 token_request("http://169.254.169.254", 21600),
546 token_response(21600, TOKEN_A),
547 ),
548 ReplayEvent::new(
549 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
550 imds_response(r#"profile-name"#),
551 ),
552 ReplayEvent::new(
553 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name", TOKEN_A),
554 imds_response("{\n \"Code\" : \"Success\",\n \"LastUpdated\" : \"2021-09-20T21:42:26Z\",\n \"Type\" : \"AWS-HMAC\",\n \"AccessKeyId\" : \"ASIARTEST\",\n \"SecretAccessKey\" : \"testsecret\",\n \"Token\" : \"testtoken\",\n \"Expiration\" : \"2021-09-21T04:16:53Z\"\n}"),
555 ),
556 ReplayEvent::new(
557 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
558 imds_response(r#"different-profile"#),
559 ),
560 ReplayEvent::new(
561 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/different-profile", TOKEN_A),
562 imds_response("{\n \"Code\" : \"Success\",\n \"LastUpdated\" : \"2021-09-20T21:42:26Z\",\n \"Type\" : \"AWS-HMAC\",\n \"AccessKeyId\" : \"ASIARTEST2\",\n \"SecretAccessKey\" : \"testsecret\",\n \"Token\" : \"testtoken\",\n \"Expiration\" : \"2021-09-21T04:16:53Z\"\n}"),
563 ),
564 ]);
565 let client = ImdsCredentialsProvider::builder()
566 .imds_client(make_imds_client(&http_client))
567 .configure(&ProviderConfig::no_configuration())
568 .build();
569 let creds = client.provide_credentials().await.expect("valid creds");
570 assert_eq!(
571 &vec![AwsCredentialFeature::CredentialsImds],
572 creds.get_property::<Vec<AwsCredentialFeature>>().unwrap()
573 );
574 }
575}