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/{profile}",
229 ))
230 .await
231 .map_err(CredentialsError::provider_error)?;
232 match parse_json_credentials(credentials.as_ref()) {
233 Ok(JsonCredentials::RefreshableCredentials(RefreshableCredentials {
234 access_key_id,
235 secret_access_key,
236 session_token,
237 account_id,
238 expiration,
239 ..
240 })) => {
241 let _ = account_id;
243 let expiration = self.maybe_extend_expiration(expiration);
244 let creds = Credentials::new(
245 access_key_id,
246 secret_access_key,
247 Some(session_token.to_string()),
248 expiration.into(),
249 "IMDSv2",
250 );
251 *self.last_retrieved_credentials.write().unwrap() = Some(creds.clone());
252 Ok(creds)
253 }
254 Ok(JsonCredentials::Error { code, message })
255 if code == codes::ASSUME_ROLE_UNAUTHORIZED_ACCESS =>
256 {
257 Err(CredentialsError::invalid_configuration(format!(
258 "Incorrect IMDS/IAM configuration: [{code}] {message}. \
259 Hint: Does this role have a trust relationship with EC2?",
260 )))
261 }
262 Ok(JsonCredentials::Error { code, message }) => Err(CredentialsError::provider_error(
263 format!("Error retrieving credentials from IMDS: {code} {message}"),
264 )),
265 Err(invalid) => Err(CredentialsError::unhandled(invalid)),
267 }
268 .map(|mut creds| {
269 creds
270 .get_property_mut_or_default::<Vec<AwsCredentialFeature>>()
271 .push(AwsCredentialFeature::CredentialsImds);
272 creds
273 })
274 }
275
276 async fn credentials(&self) -> provider::Result {
277 match self.retrieve_credentials().await {
278 creds @ Ok(_) => creds,
279 err => match &*self.last_retrieved_credentials.read().unwrap() {
281 Some(creds) => Ok(creds.clone()),
282 _ => err,
283 },
284 }
285 }
286}
287
288#[cfg(test)]
289mod test {
290 use super::*;
291 use crate::imds::client::test::{
292 imds_request, imds_response, make_imds_client, token_request, token_response,
293 };
294 use crate::provider_config::ProviderConfig;
295 use aws_credential_types::credential_feature::AwsCredentialFeature;
296 use aws_credential_types::provider::ProvideCredentials;
297 use aws_smithy_async::test_util::instant_time_and_sleep;
298 use aws_smithy_http_client::test_util::{ReplayEvent, StaticReplayClient};
299 use aws_smithy_types::body::SdkBody;
300 use std::time::{Duration, UNIX_EPOCH};
301 use tracing_test::traced_test;
302
303 const TOKEN_A: &str = "token_a";
304
305 #[tokio::test]
306 async fn profile_is_not_cached() {
307 let http_client = StaticReplayClient::new(vec![
308 ReplayEvent::new(
309 token_request("http://169.254.169.254", 21600),
310 token_response(21600, TOKEN_A),
311 ),
312 ReplayEvent::new(
313 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
314 imds_response(r#"profile-name"#),
315 ),
316 ReplayEvent::new(
317 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name", TOKEN_A),
318 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}"),
319 ),
320 ReplayEvent::new(
321 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
322 imds_response(r#"different-profile"#),
323 ),
324 ReplayEvent::new(
325 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/different-profile", TOKEN_A),
326 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}"),
327 ),
328 ]);
329 let client = ImdsCredentialsProvider::builder()
330 .imds_client(make_imds_client(&http_client))
331 .configure(&ProviderConfig::no_configuration())
332 .build();
333 let creds1 = client.provide_credentials().await.expect("valid creds");
334 let creds2 = client.provide_credentials().await.expect("valid creds");
335 assert_eq!(creds1.access_key_id(), "ASIARTEST");
336 assert_eq!(creds2.access_key_id(), "ASIARTEST2");
337 http_client.assert_requests_match(&[]);
338 }
339
340 #[tokio::test]
341 #[traced_test]
342 async fn credentials_not_stale_should_be_used_as_they_are() {
343 let http_client = StaticReplayClient::new(vec![
344 ReplayEvent::new(
345 token_request("http://169.254.169.254", 21600),
346 token_response(21600, TOKEN_A),
347 ),
348 ReplayEvent::new(
349 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
350 imds_response(r#"profile-name"#),
351 ),
352 ReplayEvent::new(
353 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name", TOKEN_A),
354 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}"),
355 ),
356 ]);
357
358 let time_of_request_to_fetch_credentials = UNIX_EPOCH + Duration::from_secs(1632197810);
361 let (time_source, sleep) = instant_time_and_sleep(time_of_request_to_fetch_credentials);
362
363 let provider_config = ProviderConfig::no_configuration()
364 .with_http_client(http_client.clone())
365 .with_sleep_impl(sleep)
366 .with_time_source(time_source);
367 let client = crate::imds::Client::builder()
368 .configure(&provider_config)
369 .build();
370 let provider = ImdsCredentialsProvider::builder()
371 .configure(&provider_config)
372 .imds_client(client)
373 .build();
374 let creds = provider.provide_credentials().await.expect("valid creds");
375 assert_eq!(
377 creds.expiry(),
378 UNIX_EPOCH.checked_add(Duration::from_secs(1632197813))
379 );
380 http_client.assert_requests_match(&[]);
381
382 assert!(!logs_contain(WARNING_FOR_EXTENDING_CREDENTIALS_EXPIRY));
384 }
385 #[tokio::test]
386 #[traced_test]
387 async fn expired_credentials_should_be_extended() {
388 let http_client = StaticReplayClient::new(vec![
389 ReplayEvent::new(
390 token_request("http://169.254.169.254", 21600),
391 token_response(21600, TOKEN_A),
392 ),
393 ReplayEvent::new(
394 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
395 imds_response(r#"profile-name"#),
396 ),
397 ReplayEvent::new(
398 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name", TOKEN_A),
399 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}"),
400 ),
401 ]);
402
403 let time_of_request_to_fetch_credentials = UNIX_EPOCH + Duration::from_secs(1632246085);
405 let (time_source, sleep) = instant_time_and_sleep(time_of_request_to_fetch_credentials);
406
407 let provider_config = ProviderConfig::no_configuration()
408 .with_http_client(http_client.clone())
409 .with_sleep_impl(sleep)
410 .with_time_source(time_source);
411 let client = crate::imds::Client::builder()
412 .configure(&provider_config)
413 .build();
414 let provider = ImdsCredentialsProvider::builder()
415 .configure(&provider_config)
416 .imds_client(client)
417 .build();
418 let creds = provider.provide_credentials().await.expect("valid creds");
419 assert!(creds.expiry().unwrap() > time_of_request_to_fetch_credentials);
420 http_client.assert_requests_match(&[]);
421
422 assert!(logs_contain(WARNING_FOR_EXTENDING_CREDENTIALS_EXPIRY));
424 }
425
426 #[tokio::test]
427 #[cfg(feature = "default-https-client")]
428 async fn read_timeout_during_credentials_refresh_should_yield_last_retrieved_credentials() {
429 let client = crate::imds::Client::builder()
430 .endpoint("http://240.0.0.0")
432 .unwrap()
433 .build();
434 let expected = aws_credential_types::Credentials::for_tests();
435 let provider = ImdsCredentialsProvider::builder()
436 .imds_client(client)
437 .last_retrieved_credentials(expected.clone())
439 .build();
440 let actual = provider.provide_credentials().await;
441 assert_eq!(actual.unwrap(), expected);
442 }
443
444 #[tokio::test]
445 #[cfg(feature = "default-https-client")]
446 async fn read_timeout_during_credentials_refresh_should_error_without_last_retrieved_credentials(
447 ) {
448 let client = crate::imds::Client::builder()
449 .endpoint("http://240.0.0.0")
451 .unwrap()
452 .build();
453 let provider = ImdsCredentialsProvider::builder()
454 .imds_client(client)
455 .build();
457 let actual = provider.provide_credentials().await;
458 assert!(
459 matches!(actual, Err(CredentialsError::CredentialsNotLoaded(_))),
460 "\nexpected: Err(CredentialsError::CredentialsNotLoaded(_))\nactual: {actual:?}"
461 );
462 }
463
464 #[cfg_attr(windows, ignore)]
466 #[tokio::test]
467 #[cfg(feature = "default-https-client")]
468 async fn external_timeout_during_credentials_refresh_should_yield_last_retrieved_credentials() {
469 use aws_smithy_async::rt::sleep::AsyncSleep;
470 let client = crate::imds::Client::builder()
471 .endpoint("http://240.0.0.0")
473 .unwrap()
474 .build();
475 let expected = aws_credential_types::Credentials::for_tests();
476 let provider = ImdsCredentialsProvider::builder()
477 .imds_client(client)
478 .configure(&ProviderConfig::no_configuration())
479 .last_retrieved_credentials(expected.clone())
481 .build();
482 let sleeper = aws_smithy_async::rt::sleep::TokioSleep::new();
483 let timeout = aws_smithy_async::future::timeout::Timeout::new(
484 provider.provide_credentials(),
485 sleeper.sleep(std::time::Duration::from_millis(100)),
487 );
488 match timeout.await {
489 Ok(_) => panic!("provide_credentials completed before timeout future"),
490 Err(_err) => match provider.fallback_on_interrupt() {
491 Some(actual) => assert_eq!(actual, expected),
492 None => panic!(
493 "provide_credentials timed out and no credentials returned from fallback_on_interrupt"
494 ),
495 },
496 };
497 }
498
499 #[tokio::test]
500 async fn fallback_credentials_should_be_used_when_imds_returns_500_during_credentials_refresh()
501 {
502 let http_client = StaticReplayClient::new(vec![
503 ReplayEvent::new(
506 token_request("http://169.254.169.254", 21600),
507 token_response(21600, TOKEN_A),
508 ),
509 ReplayEvent::new(
510 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
511 imds_response(r#"profile-name"#),
512 ),
513 ReplayEvent::new(
514 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name", TOKEN_A),
515 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}"),
516 ),
517 ReplayEvent::new(
520 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
521 http::Response::builder().status(500).body(SdkBody::empty()).unwrap(),
522 ),
523 ]);
524 let provider = ImdsCredentialsProvider::builder()
525 .imds_client(make_imds_client(&http_client))
526 .configure(&ProviderConfig::no_configuration())
527 .build();
528 let creds1 = provider.provide_credentials().await.expect("valid creds");
529 assert_eq!(creds1.access_key_id(), "ASIARTEST");
530 let creds2 = provider.provide_credentials().await.expect("valid creds");
532 assert_eq!(creds1, creds2);
533 http_client.assert_requests_match(&[]);
534 }
535
536 #[tokio::test]
537 async fn credentials_feature() {
538 let http_client = StaticReplayClient::new(vec![
539 ReplayEvent::new(
540 token_request("http://169.254.169.254", 21600),
541 token_response(21600, TOKEN_A),
542 ),
543 ReplayEvent::new(
544 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
545 imds_response(r#"profile-name"#),
546 ),
547 ReplayEvent::new(
548 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name", TOKEN_A),
549 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}"),
550 ),
551 ReplayEvent::new(
552 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
553 imds_response(r#"different-profile"#),
554 ),
555 ReplayEvent::new(
556 imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/different-profile", TOKEN_A),
557 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}"),
558 ),
559 ]);
560 let client = ImdsCredentialsProvider::builder()
561 .imds_client(make_imds_client(&http_client))
562 .configure(&ProviderConfig::no_configuration())
563 .build();
564 let creds = client.provide_credentials().await.expect("valid creds");
565 assert_eq!(
566 &vec![AwsCredentialFeature::CredentialsImds],
567 creds.get_property::<Vec<AwsCredentialFeature>>().unwrap()
568 );
569 }
570}