-
Notifications
You must be signed in to change notification settings - Fork 244
Add support for AWS_CONTAINER_CREDENTIALS_FULL_URI (EKS Pod Identity Agent) #449
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
e17241a
6d9d6f2
7279822
4462583
550f2c8
31bd7d8
811c424
d088dde
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,7 +12,7 @@ use std::sync::atomic::AtomicU32; | |
| use std::sync::atomic::Ordering; | ||
| use std::time::Duration; | ||
| use time::OffsetDateTime; | ||
| use url::Url; | ||
| use url::{Host, Url}; | ||
|
|
||
| /// AWS access credentials: access key, secret key, and optional token. | ||
| /// | ||
|
|
@@ -187,6 +187,48 @@ fn http_get(url: &str) -> attohttpc::Result<attohttpc::Response> { | |
| builder.send() | ||
| } | ||
|
|
||
| // EKS Pod Identity Agent link-local addresses documented by AWS: | ||
| // https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html#pod-id-considerations | ||
| #[cfg(feature = "http-credentials")] | ||
| const EKS_POD_IDENTITY_AGENT_IPV4: [u8; 4] = [169, 254, 170, 23]; | ||
| #[cfg(feature = "http-credentials")] | ||
| const EKS_POD_IDENTITY_AGENT_IPV6: [u16; 8] = [0xfd00, 0x0ec2, 0, 0, 0, 0, 0, 0x23]; | ||
|
|
||
| /// 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() -> Result<Option<String>, CredentialsError> { | ||
| if let Ok(path) = env::var("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE") { | ||
| let token = std::fs::read_to_string(path)?; | ||
| return Ok(Some(token.trim_end().to_string())); | ||
| } | ||
| Ok(env::var("AWS_CONTAINER_AUTHORIZATION_TOKEN").ok()) | ||
| } | ||
|
|
||
| /// Returns whether it is safe to send a container authorization token to `url`. | ||
| #[cfg(feature = "http-credentials")] | ||
| fn validate_container_credentials_full_uri_for_auth_token( | ||
| url: &str, | ||
| ) -> Result<(), CredentialsError> { | ||
| let parsed = Url::parse(url)?; | ||
|
|
||
| if parsed.scheme() != "http" && parsed.scheme() != "https" { | ||
| return Err(CredentialsError::InvalidContainerCredentialsFullUri( | ||
| url.to_string(), | ||
| )); | ||
| } | ||
|
|
||
| match parsed.host() { | ||
| Some(Host::Ipv4(ip)) if ip.octets() == EKS_POD_IDENTITY_AGENT_IPV4 => Ok(()), | ||
| Some(Host::Ipv6(ip)) if ip.segments() == EKS_POD_IDENTITY_AGENT_IPV6 => Ok(()), | ||
| _ => Err(CredentialsError::InvalidContainerCredentialsFullUri( | ||
| url.to_string(), | ||
| )), | ||
| } | ||
| } | ||
|
Comment on lines
+210
to
+230
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: The AWS Container Credential Provider uses the AWS_CONTAINER_CREDENTIALS_FULL_URI environment variable to specify a full HTTP/HTTPS endpoint for fetching credentials in container environments like ECS and EKS. Due to security policies across AWS SDKs, this URI is restricted: For HTTP URIs: Only loopback hosts (localhost, 127.0.0.1, ::1) and specific AWS service endpoints (e.g., ECS 169.254.170.2, EKS IPv6 like fd00:ec2::23) are allowed. Non-loopback HTTP URIs are rejected. For HTTPS URIs: All hosts are permitted. This policy is enforced consistently: - Java SDK v1 (1.x): Only loopback hosts for HTTP; all HTTPS allowed. Docs explicitly state: "For absolute paths, only loopback hosts are allowed when using HTTP... All HTTPS endpoints are allowed." {1,5} - Java SDK v2 (2.x): Validates host resolves to loopback addresses for HTTP (via InetAddress check); HTTPS unrestricted. Source code confirms: if not HTTPS and not allowed host (loopback rules), throws error. {20} - Go SDK (v1 and v2): Follows same standard; supports FULL_URI with loopback/known endpoints for HTTP. {14,15} - Botocore (Python/CLI): ContainerMetadataFetcher._ALLOWED_HOSTS limits to ['169.254.170.2', 'localhost', '127.0.0.1'] for FULL_URI; cannot extend due to org-wide security policy. {2,16} This is a deliberate cross-SDK security measure to prevent arbitrary remote credential fetches, as confirmed in GitHub issues. {2,4} No configuration to override allowed hosts; use relative URI (AWS_CONTAINER_CREDENTIALS_RELATIVE_URI) with default ECS host or ensure HTTPS/loopback compliance. IPv6 loopback supported in recent versions. {13,23} Citations:
The current implementation only permits
This breaks:
Correct policy: HTTPS → allow any host (transport encrypted); HTTP → restrict to loopback ( 🛡️ Suggested allowlist expansion fn validate_container_credentials_full_uri_for_auth_token(
url: &str,
) -> Result<(), CredentialsError> {
let parsed = Url::parse(url)?;
if parsed.scheme() != "http" && parsed.scheme() != "https" {
return Err(CredentialsError::InvalidContainerCredentialsFullUri(
url.to_string(),
));
}
+ // All HTTPS endpoints are trusted (transport security covers the token).
+ if parsed.scheme() == "https" {
+ return Ok(());
+ }
+
+ // For HTTP, restrict to loopback and the known EKS/ECS link-local IPs.
match parsed.host() {
Some(Host::Ipv4(ip)) if ip.octets() == EKS_POD_IDENTITY_AGENT_IPV4 => Ok(()),
Some(Host::Ipv6(ip)) if ip.segments() == EKS_POD_IDENTITY_AGENT_IPV6 => Ok(()),
+ Some(Host::Ipv4(ip)) if ip.is_loopback() => Ok(()),
+ Some(Host::Ipv6(ip)) if ip.is_loopback() => Ok(()),
+ Some(Host::Domain(d)) if d == "localhost" => Ok(()),
_ => Err(CredentialsError::InvalidContainerCredentialsFullUri(
url.to_string(),
)),
}
}🤖 Prompt for AI Agents
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wholeheartedly disagree. The PRs you reference are people asking for support to point to other URIs for the credentials address, for which in all 3 PRs AWS employees say the same answer - that's it's against an org-wide (AWS-wide) security policy to allow that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Looking at the actual AWS Java SDK documentation more carefully, the policy is: "For absolute paths, only loopback hosts are allowed when using HTTP, including known endpoints for ECS and EKS. All HTTPS endpoints are allowed." The Java SDK v2 enforces the same rule, rejecting endpoints where the "host should resolve to a loopback address or have the full URI be HTTPS." That said, the PR maintainer ( I withdraw my suggestion to expand the allowlist. The current implementation is a valid and defensible conservative choice. ✏️ Learnings added
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 😆 |
||
|
|
||
| impl Credentials { | ||
| pub fn refresh(&mut self) -> Result<(), CredentialsError> { | ||
| if let Some(expiration) = self.expiration { | ||
|
|
@@ -327,18 +369,53 @@ 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`. | ||
| /// | ||
| /// # Example | ||
| /// | ||
| /// ```no_run | ||
| /// use awscreds::Credentials; | ||
| /// | ||
| /// // ECS task role credentials use a relative path on the ECS metadata host. | ||
| /// std::env::set_var("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/v2/credentials/task-role"); | ||
| /// let credentials = Credentials::from_container_credentials_provider()?; | ||
| /// | ||
| /// // EKS Pod Identity uses a full URI and may require an authorization token. | ||
| /// std::env::remove_var("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"); | ||
| /// std::env::set_var( | ||
| /// "AWS_CONTAINER_CREDENTIALS_FULL_URI", | ||
| /// "http://169.254.170.23/v1/credentials", | ||
| /// ); | ||
| /// std::env::set_var("AWS_CONTAINER_AUTHORIZATION_TOKEN", "pod-identity-token"); | ||
| /// let credentials = Credentials::from_container_credentials_provider()?; | ||
| /// # Ok::<(), awscreds::error::CredentialsError>(()) | ||
| /// ``` | ||
| #[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()?; | ||
| if token.is_some() { | ||
| validate_container_credentials_full_uri_for_auth_token(&full_uri)?; | ||
| } | ||
| (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()); | ||
| } | ||
|
LockedThread marked this conversation as resolved.
|
||
|
|
||
| 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), | ||
|
|
@@ -510,8 +587,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(); | ||
|
|
@@ -549,15 +632,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] | ||
|
|
@@ -578,6 +657,235 @@ 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().unwrap(), | ||
| Some("token-from-file".to_string()) | ||
| ); | ||
| } | ||
|
|
||
| #[cfg(feature = "http-credentials")] | ||
| #[test] | ||
| fn test_get_container_authorization_token_file_error_does_not_fallback_to_env() { | ||
| let _lock = CONTAINER_CREDENTIALS_TEST_LOCK | ||
| .get_or_init(|| Mutex::new(())) | ||
| .lock() | ||
| .unwrap(); | ||
| let _guard_file = EnvGuard::set( | ||
| "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", | ||
| "/path/that/does/not/exist/token", | ||
| ); | ||
| let _guard_env = EnvGuard::set("AWS_CONTAINER_AUTHORIZATION_TOKEN", "token-from-env"); | ||
|
|
||
| let result = get_container_authorization_token(); | ||
| assert!(matches!(result, Err(CredentialsError::Io(_)))); | ||
| } | ||
|
|
||
| #[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().unwrap(), | ||
| 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().unwrap(), | ||
| Some("token-from-file".to_string()) | ||
| ); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| #[cfg(feature = "http-credentials")] | ||
| #[test] | ||
| fn test_container_credentials_rejects_auth_token_for_untrusted_full_uri() { | ||
| let _lock = CONTAINER_CREDENTIALS_TEST_LOCK | ||
| .get_or_init(|| Mutex::new(())) | ||
| .lock() | ||
| .unwrap(); | ||
| let _rel = EnvGuard::remove(&["AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"]); | ||
| let _full = EnvGuard::set( | ||
| "AWS_CONTAINER_CREDENTIALS_FULL_URI", | ||
| "https://example.com/v1/credentials", | ||
| ); | ||
| let _token = EnvGuard::set("AWS_CONTAINER_AUTHORIZATION_TOKEN", "token-from-env"); | ||
| let _file = EnvGuard::remove(&["AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE"]); | ||
|
|
||
| let result = Credentials::from_container_credentials_provider(); | ||
| assert!(matches!( | ||
| result, | ||
| Err(CredentialsError::InvalidContainerCredentialsFullUri(_)) | ||
| )); | ||
| } | ||
|
|
||
| #[cfg(feature = "http-credentials")] | ||
| #[test] | ||
| fn test_container_credentials_auth_token_full_uri_allowlist() { | ||
| assert!(validate_container_credentials_full_uri_for_auth_token( | ||
| "http://169.254.170.23/v1/credentials", | ||
| ) | ||
| .is_ok()); | ||
| assert!(validate_container_credentials_full_uri_for_auth_token( | ||
| "http://[fd00:ec2::23]/v1/credentials", | ||
| ) | ||
| .is_ok()); | ||
| assert!(matches!( | ||
| validate_container_credentials_full_uri_for_auth_token( | ||
| "http://169.254.170.24/v1/credentials", | ||
| ), | ||
| Err(CredentialsError::InvalidContainerCredentialsFullUri(_)) | ||
| )); | ||
| assert!(matches!( | ||
| validate_container_credentials_full_uri_for_auth_token( | ||
| "http://localhost/v1/credentials", | ||
| ), | ||
| Err(CredentialsError::InvalidContainerCredentialsFullUri(_)) | ||
| )); | ||
| assert!(matches!( | ||
| validate_container_credentials_full_uri_for_auth_token( | ||
| "http://127.0.0.1/v1/credentials", | ||
| ), | ||
| Err(CredentialsError::InvalidContainerCredentialsFullUri(_)) | ||
| )); | ||
| assert!(matches!( | ||
| validate_container_credentials_full_uri_for_auth_token( | ||
| "ftp://localhost/v1/credentials", | ||
| ), | ||
| Err(CredentialsError::InvalidContainerCredentialsFullUri(_)) | ||
| )); | ||
| } | ||
| } | ||
|
|
||
| /// 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)] | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
trim_end()leaves potential leading whitespace in file-sourced tokens; the env-var path is untrimmed entirely.token.trim_end().to_string()on line 205 strips trailing whitespace (the common trailing newline) but does not strip leading whitespace. AWS CLI's equivalent reads the file and does a fullstrip(). If a token file has leading whitespace due to a template or editor artefact, the value passed in theAuthorizationheader will be silently corrupted.Additionally, the env-var path at line 207 applies no trimming at all, while the file path applies partial trimming—an inconsistency that can lead to subtle header-value mismatches.
✏️ Proposed fix
🤖 Prompt for AI Agents