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