From e17241a2ccdb40ab1ae0029a2809555e9c7fc1ab Mon Sep 17 00:00:00 2001 From: Warren Snipes Date: Tue, 3 Mar 2026 00:13:23 -0500 Subject: [PATCH 1/5] Add support for EKS Pod Identity --- aws-creds/src/credentials.rs | 44 ++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/aws-creds/src/credentials.rs b/aws-creds/src/credentials.rs index 020477b753..1146ecd6bb 100644 --- a/aws-creds/src/credentials.rs +++ b/aws-creds/src/credentials.rs @@ -187,6 +187,20 @@ fn http_get(url: &str) -> attohttpc::Result { builder.send() } +/// Reads the container authorization token from environment. +/// Checks `AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE` first (file path), then +/// `AWS_CONTAINER_AUTHORIZATION_TOKEN`. Used when fetching credentials from +/// `AWS_CONTAINER_CREDENTIALS_FULL_URI` (e.g. EKS Pod Identity Agent). +#[cfg(feature = "http-credentials")] +fn get_container_authorization_token() -> Option { + if let Ok(path) = env::var("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE") { + if let Ok(token) = std::fs::read_to_string(path) { + return Some(token.trim_end().to_string()); + } + } + env::var("AWS_CONTAINER_AUTHORIZATION_TOKEN").ok() +} + impl Credentials { pub fn refresh(&mut self) -> Result<(), CredentialsError> { if let Some(expiration) = self.expiration { @@ -327,18 +341,30 @@ impl Credentials { Credentials::from_env_specific(None, None, None, None) } + /// Load credentials from container metadata (ECS task role or EKS Pod Identity). + /// + /// Checks `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` first (ECS), then + /// `AWS_CONTAINER_CREDENTIALS_FULL_URI` (EKS Pod Identity Agent, etc.). + /// When using `FULL_URI`, optionally sends an `Authorization` header from + /// `AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE` or `AWS_CONTAINER_AUTHORIZATION_TOKEN`. #[cfg(feature = "http-credentials")] pub fn from_container_credentials_provider() -> Result { - let Ok(credentials_path) = env::var("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") else { - return Err(CredentialsError::NotContainer); - }; + let (url, auth_token) = + if let Ok(relative_uri) = env::var("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") { + (format!("http://169.254.170.2{}", relative_uri), None) + } else if let Ok(full_uri) = env::var("AWS_CONTAINER_CREDENTIALS_FULL_URI") { + let token = get_container_authorization_token(); + (full_uri, token) + } else { + return Err(CredentialsError::NotContainer); + }; + + let mut request = apply_timeout(attohttpc::get(&url)); + if let Some(ref token) = auth_token { + request = request.header("Authorization", token.as_str()); + } - let resp: CredentialsFromInstanceMetadata = apply_timeout(attohttpc::get(format!( - "http://169.254.170.2{}", - credentials_path - ))) - .send()? - .json()?; + let resp: CredentialsFromInstanceMetadata = request.send()?.json()?; Ok(Credentials { access_key: Some(resp.access_key_id), From 6d9d6f2788dbc9db2efb7e05b0f72530eee63472 Mon Sep 17 00:00:00 2001 From: Warren Snipes Date: Tue, 3 Mar 2026 00:13:34 -0500 Subject: [PATCH 2/5] Add tests --- aws-creds/src/credentials.rs | 129 +++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/aws-creds/src/credentials.rs b/aws-creds/src/credentials.rs index 1146ecd6bb..30cb7d4ade 100644 --- a/aws-creds/src/credentials.rs +++ b/aws-creds/src/credentials.rs @@ -604,6 +604,135 @@ aws_secret_access_key = SECRET CredentialsError::ConfigNotFound )); } + + // Container credentials (RELATIVE_URI, FULL_URI, authorization token) - require http-credentials + #[cfg(feature = "http-credentials")] + #[test] + fn test_container_credentials_not_container_when_no_env() { + let _guard = EnvGuard::remove(&[ + "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", + "AWS_CONTAINER_CREDENTIALS_FULL_URI", + ]); + let result = Credentials::from_container_credentials_provider(); + assert!(matches!(result, Err(CredentialsError::NotContainer))); + } + + #[cfg(feature = "http-credentials")] + #[test] + fn test_container_credentials_relative_uri_precedence() { + let _guard = EnvGuard::set( + "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", + "/v2/credentials/x", + ); + let _guard2 = EnvGuard::set( + "AWS_CONTAINER_CREDENTIALS_FULL_URI", + "http://169.254.170.23/v1/credentials", + ); + // With both set, RELATIVE_URI is used. Short timeout so request fails fast (no server). + let _timeout = set_request_timeout(Some(Duration::from_millis(10))); + let result = Credentials::from_container_credentials_provider(); + let _ = set_request_timeout(_timeout); + assert!(result.is_err()); + assert!(!matches!(result, Err(CredentialsError::NotContainer))); + } + + #[cfg(feature = "http-credentials")] + #[test] + fn test_container_credentials_full_uri_used_when_no_relative() { + let _guard = EnvGuard::set( + "AWS_CONTAINER_CREDENTIALS_FULL_URI", + "http://169.254.170.23/v1/credentials", + ); + let _rel = EnvGuard::remove(&["AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"]); + // FULL_URI is used; short timeout so request fails fast (no server). + let _timeout = set_request_timeout(Some(Duration::from_millis(10))); + let result = Credentials::from_container_credentials_provider(); + let _ = set_request_timeout(_timeout); + assert!(result.is_err()); + assert!(!matches!(result, Err(CredentialsError::NotContainer))); + } + + #[cfg(feature = "http-credentials")] + #[test] + fn test_get_container_authorization_token_from_file() { + let mut file = NamedTempFile::new().unwrap(); + file.write_all(b"token-from-file").unwrap(); + file.flush().unwrap(); + let path = file.path().to_string_lossy().to_string(); + let _guard = EnvGuard::set("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", &path); + let _u = EnvGuard::remove(&["AWS_CONTAINER_AUTHORIZATION_TOKEN"]); + assert_eq!( + get_container_authorization_token(), + Some("token-from-file".to_string()) + ); + } + + #[cfg(feature = "http-credentials")] + #[test] + fn test_get_container_authorization_token_from_env() { + let _guard = EnvGuard::set("AWS_CONTAINER_AUTHORIZATION_TOKEN", "token-from-env"); + let _u = EnvGuard::remove(&["AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE"]); + assert_eq!( + get_container_authorization_token(), + Some("token-from-env".to_string()) + ); + } + + #[cfg(feature = "http-credentials")] + #[test] + fn test_get_container_authorization_token_file_precedence() { + let mut file = NamedTempFile::new().unwrap(); + file.write_all(b"token-from-file").unwrap(); + file.flush().unwrap(); + let path = file.path().to_string_lossy().to_string(); + let _guard_file = EnvGuard::set("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", &path); + let _guard_env = EnvGuard::set("AWS_CONTAINER_AUTHORIZATION_TOKEN", "token-from-env"); + // File takes precedence over env var. + assert_eq!( + get_container_authorization_token(), + Some("token-from-file".to_string()) + ); + } +} + +/// Restores env vars when dropped. Used in tests to avoid leaking env state. +#[cfg(test)] +struct EnvGuard { + saved: Vec<(String, Option)>, +} + +#[cfg(test)] +impl EnvGuard { + fn set(key: &str, value: &str) -> Self { + let saved = env::var(key).ok(); + env::set_var(key, value); + Self { + saved: vec![(key.to_string(), saved)], + } + } + fn remove(keys: &[&str]) -> Self { + let mut saved = Vec::with_capacity(keys.len()); + for key in keys { + let key = (*key).to_string(); + let val = env::var(&key).ok(); + env::remove_var(&key); + saved.push((key, val)); + } + Self { saved } + } +} + +#[cfg(test)] +impl Drop for EnvGuard { + fn drop(&mut self) { + for (key, value) in &self.saved { + if let Some(ref v) = value { + env::set_var(key, v); + } else { + env::remove_var(key); + } + } + } } #[cfg(test)] From 7279822a2b988eed2d08151913f8f06a1e906fab Mon Sep 17 00:00:00 2001 From: Warren Snipes Date: Tue, 3 Mar 2026 00:13:56 -0500 Subject: [PATCH 3/5] Use EnvGuard for existing env-related tests --- aws-creds/src/credentials.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/aws-creds/src/credentials.rs b/aws-creds/src/credentials.rs index 30cb7d4ade..5b92093287 100644 --- a/aws-creds/src/credentials.rs +++ b/aws-creds/src/credentials.rs @@ -575,15 +575,11 @@ aws_access_key_id = ENV_KEY aws_secret_access_key = ENV_SECRET "#; let file = create_test_credentials_file(content); - - // Set the environment variable - env::set_var("AWS_SHARED_CREDENTIALS_FILE", file.path()); + let path = file.path().to_string_lossy().to_string(); + let _guard = EnvGuard::set("AWS_SHARED_CREDENTIALS_FILE", &path); let creds = Credentials::from_profile(None).unwrap(); assert_eq!(creds.access_key.unwrap(), "ENV_KEY"); - - // Clean up - env::remove_var("AWS_SHARED_CREDENTIALS_FILE"); } #[test] From 4462583dd6662000e1271003e7103af3b8441fab Mon Sep 17 00:00:00 2001 From: Warren Snipes Date: Tue, 3 Mar 2026 00:18:46 -0500 Subject: [PATCH 4/5] Fix leaky tests by using lock --- aws-creds/src/credentials.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/aws-creds/src/credentials.rs b/aws-creds/src/credentials.rs index 5b92093287..7dde261d2b 100644 --- a/aws-creds/src/credentials.rs +++ b/aws-creds/src/credentials.rs @@ -536,8 +536,14 @@ struct CredentialsFromInstanceMetadata { mod tests { use super::*; use std::io::Write; + use std::sync::Mutex; + use std::sync::OnceLock; use tempfile::NamedTempFile; + /// Serializes container-credentials tests that touch RELATIVE_URI/FULL_URI so parallel runs don't race. + #[cfg(feature = "http-credentials")] + static CONTAINER_CREDENTIALS_TEST_LOCK: OnceLock> = OnceLock::new(); + fn create_test_credentials_file(content: &str) -> NamedTempFile { let mut file = NamedTempFile::new().unwrap(); file.write_all(content.as_bytes()).unwrap(); @@ -605,6 +611,10 @@ aws_secret_access_key = SECRET #[cfg(feature = "http-credentials")] #[test] fn test_container_credentials_not_container_when_no_env() { + let _lock = CONTAINER_CREDENTIALS_TEST_LOCK + .get_or_init(|| Mutex::new(())) + .lock() + .unwrap(); let _guard = EnvGuard::remove(&[ "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "AWS_CONTAINER_CREDENTIALS_FULL_URI", @@ -616,6 +626,10 @@ aws_secret_access_key = SECRET #[cfg(feature = "http-credentials")] #[test] fn test_container_credentials_relative_uri_precedence() { + let _lock = CONTAINER_CREDENTIALS_TEST_LOCK + .get_or_init(|| Mutex::new(())) + .lock() + .unwrap(); let _guard = EnvGuard::set( "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/v2/credentials/x", @@ -635,6 +649,10 @@ aws_secret_access_key = SECRET #[cfg(feature = "http-credentials")] #[test] fn test_container_credentials_full_uri_used_when_no_relative() { + let _lock = CONTAINER_CREDENTIALS_TEST_LOCK + .get_or_init(|| Mutex::new(())) + .lock() + .unwrap(); let _guard = EnvGuard::set( "AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://169.254.170.23/v1/credentials", From 550f2c89379e83385e5fb0ee04afe3492f653757 Mon Sep 17 00:00:00 2001 From: Warren Snipes Date: Tue, 3 Mar 2026 00:40:12 -0500 Subject: [PATCH 5/5] Add locks for token-env tests to remove chance of leaky tests --- aws-creds/src/credentials.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/aws-creds/src/credentials.rs b/aws-creds/src/credentials.rs index 7dde261d2b..fd494c6881 100644 --- a/aws-creds/src/credentials.rs +++ b/aws-creds/src/credentials.rs @@ -669,6 +669,10 @@ aws_secret_access_key = SECRET #[cfg(feature = "http-credentials")] #[test] fn test_get_container_authorization_token_from_file() { + let _lock = CONTAINER_CREDENTIALS_TEST_LOCK + .get_or_init(|| Mutex::new(())) + .lock() + .unwrap(); let mut file = NamedTempFile::new().unwrap(); file.write_all(b"token-from-file").unwrap(); file.flush().unwrap(); @@ -684,6 +688,10 @@ aws_secret_access_key = SECRET #[cfg(feature = "http-credentials")] #[test] fn test_get_container_authorization_token_from_env() { + let _lock = CONTAINER_CREDENTIALS_TEST_LOCK + .get_or_init(|| Mutex::new(())) + .lock() + .unwrap(); let _guard = EnvGuard::set("AWS_CONTAINER_AUTHORIZATION_TOKEN", "token-from-env"); let _u = EnvGuard::remove(&["AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE"]); assert_eq!( @@ -695,6 +703,10 @@ aws_secret_access_key = SECRET #[cfg(feature = "http-credentials")] #[test] fn test_get_container_authorization_token_file_precedence() { + let _lock = CONTAINER_CREDENTIALS_TEST_LOCK + .get_or_init(|| Mutex::new(())) + .lock() + .unwrap(); let mut file = NamedTempFile::new().unwrap(); file.write_all(b"token-from-file").unwrap(); file.flush().unwrap();