Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 196 additions & 15 deletions aws-creds/src/credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,20 @@ fn http_get(url: &str) -> attohttpc::Result<attohttpc::Response> {
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<String> {
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 {
Expand Down Expand Up @@ -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<Credentials, CredentialsError> {
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),
Expand Down Expand Up @@ -510,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<Mutex<()>> = OnceLock::new();

fn create_test_credentials_file(content: &str) -> NamedTempFile {
let mut file = NamedTempFile::new().unwrap();
file.write_all(content.as_bytes()).unwrap();
Expand Down Expand Up @@ -549,15 +581,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]
Expand All @@ -578,6 +606,159 @@ 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 _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",
]);
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 _lock = CONTAINER_CREDENTIALS_TEST_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.unwrap();
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 _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",
);
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 _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();
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 _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!(
get_container_authorization_token(),
Some("token-from-env".to_string())
);
}

#[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();
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<String>)>,
}

#[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)]
Expand Down