在ECS/Fargate的AWS Batch作业中获取Job Role凭证的方案
解决方案:ECS/Fargate上Batch作业获取Job Role凭证及Rust手动实现AssumeRole
一、凭证来源解析
你发现的http://169.254.170.2/v2/credentials/UUID是**ECS任务元数据服务(v2)**的凭证获取端点,其中的UUID是当前任务的角色凭证ID——由Fargate在启动容器时自动注入,可直接读取容器内的文件拿到:
cat /run/secrets/ecs/credentials/id
访问该端点返回的就是Job Role的临时凭证(包含AccessKeyId、SecretAccessKey、SessionToken和过期时间),这也是AWS CLI能自动获取凭证的核心原因。
二、无需AssumeRole:直接获取Job Role凭证
在Rust里无需依赖AWS SDK,直接发起HTTP请求即可拿到凭证:
use reqwest::blocking::Client; use std::fs; use serde_json::Value; fn get_job_role_credentials() -> Result<Value, Box<dyn std::error::Error>> { // 读取凭证ID let cred_id = fs::read_to_string("/run/secrets/ecs/credentials/id")?.trim().to_string(); // 调用元数据端点 let client = Client::new(); let resp = client.get(format!("http://169.254.170.2/v2/credentials/{}", cred_id)) .send()? .json()?; Ok(resp) }
返回的JSON结构示例:
{ "AccessKeyId": "ASIAXXX", "SecretAccessKey": "XXX", "SessionToken": "XXX", "Expiration": "2024-05-20T12:34:56Z" }
三、必须调用AssumeRole:Rust手动实现AWS签名v4
如果需要用当前Job Role去Assume另一个角色,需手动实现AWS Signature Version 4完成请求签名,步骤如下:
1. 核心逻辑说明
- 用上述拿到的Job Role临时凭证作为签名密钥
- 构造STS的AssumeRole请求参数
- 按照SigV4规范生成签名并添加到请求头
2. Rust代码示例(仅依赖基础加密/HTTP库,无AWS SDK)
use hmac::{Hmac, Mac}; use reqwest::blocking::Client; use sha2::Sha256; use std::collections::BTreeMap; use chrono::Utc; use hex::encode; use urlencoding::encode; use roxmltree::Document; use serde_json::Value; type HmacSha256 = Hmac<Sha256>; // 生成SigV4签名密钥 fn get_signing_key(secret_key: &str, date: &str, region: &str, service: &str) -> Vec<u8> { let k_date = HmacSha256::new_from_slice(format!("AWS4{}", secret_key).as_bytes()) .unwrap() .chain_update(date.as_bytes()) .finalize() .into_bytes(); let k_region = HmacSha256::new_from_slice(&k_date) .unwrap() .chain_update(region.as_bytes()) .finalize() .into_bytes(); let k_service = HmacSha256::new_from_slice(&k_region) .unwrap() .chain_update(service.as_bytes()) .finalize() .into_bytes(); HmacSha256::new_from_slice(&k_service) .unwrap() .chain_update("aws4_request".as_bytes()) .finalize() .into_bytes() .to_vec() } // 生成规范请求字符串 fn get_canonical_request( method: &str, uri: &str, query_params: &BTreeMap<&str, &str>, headers: &BTreeMap<&str, &str>, payload_hash: &str, ) -> String { let canonical_query = query_params .iter() .map(|(k, v)| format!("{}={}", encode(k), encode(v))) .collect::<Vec<_>>() .join("&"); let canonical_headers = headers .iter() .map(|(k, v)| format!("{}:{}\n", k.to_lowercase(), v.trim())) .collect::<String>(); let signed_headers = headers .keys() .map(|k| k.to_lowercase()) .collect::<Vec<_>>() .join(";"); format!( "{}\n{}\n{}\n{}\n{}\n{}", method, uri, canonical_query, canonical_headers, signed_headers, payload_hash ) } // 执行AssumeRole请求 fn assume_role( job_role_creds: &Value, role_arn: &str, session_name: &str, region: &str, ) -> Result<Value, Box<dyn std::error::Error>> { let access_key = job_role_creds["AccessKeyId"].as_str().unwrap(); let secret_key = job_role_creds["SecretAccessKey"].as_str().unwrap(); let session_token = job_role_creds["SessionToken"].as_str().unwrap(); // 生成SigV4要求的时间格式 let datetime_str = Utc::now().format("%Y%m%dT%H%M%SZ").to_string(); let date_str = Utc::now().format("%Y%m%d").to_string(); // STS请求参数 let mut query_params = BTreeMap::new(); query_params.insert("Action", "AssumeRole"); query_params.insert("RoleArn", role_arn); query_params.insert("RoleSessionName", session_name); query_params.insert("Version", "2011-06-15"); // 请求头 let mut headers = BTreeMap::new(); headers.insert("Host", "sts.amazonaws.com"); headers.insert("X-Amz-Date", &datetime_str); headers.insert("X-Amz-Security-Token", session_token); // 计算空payload的哈希 let payload_hash = encode(Sha256::digest("".as_bytes())); // 构造规范请求和待签名字符串 let canonical_request = get_canonical_request("GET", "/", &query_params, &headers, &payload_hash); let credential_scope = format!("{}/{}/sts/aws4_request", date_str, region); let string_to_sign = format!( "AWS4-HMAC-SHA256\n{}\n{}\n{}", datetime_str, credential_scope, encode(Sha256::digest(canonical_request.as_bytes())) ); // 生成签名 let signing_key = get_signing_key(secret_key, &date_str, region, "sts"); let signature = encode(HmacSha256::new_from_slice(&signing_key) .unwrap() .chain_update(string_to_sign.as_bytes()) .finalize() .into_bytes()); // 构造Authorization头 let auth_header = format!( "AWS4-HMAC-SHA256 Credential={}/{}, SignedHeaders={}, Signature={}", access_key, credential_scope, headers.keys().map(|k| k.to_lowercase()).collect::<Vec<_>>().join(";"), signature ); // 发起请求并解析XML响应 let client = Client::new(); let resp = client.get("https://sts.amazonaws.com") .query(&query_params) .header("X-Amz-Date", datetime_str) .header("X-Amz-Security-Token", session_token) .header("Authorization", auth_header) .send()? .text()?; let xml_doc = Document::parse(&resp)?; let mut result = serde_json::Map::new(); if let Some(elem) = xml_doc.descendants().find(|n| n.tag_name().name() == "AccessKeyId") { result.insert("AccessKeyId".into(), elem.text().unwrap_or("").into()); } if let Some(elem) = xml_doc.descendants().find(|n| n.tag_name().name() == "SecretAccessKey") { result.insert("SecretAccessKey".into(), elem.text().unwrap_or("").into()); } if let Some(elem) = xml_doc.descendants().find(|n| n.tag_name().name() == "SessionToken") { result.insert("SessionToken".into(), elem.text().unwrap_or("").into()); } if let Some(elem) = xml_doc.descendants().find(|n| n.tag_name().name() == "Expiration") { result.insert("Expiration".into(), elem.text().unwrap_or("").into()); } Ok(Value::Object(result)) }
需在Cargo.toml中添加依赖:
[dependencies] reqwest = { version = "0.11", features = ["blocking"] } serde_json = "1.0" sha2 = "0.10" hmac = "0.12" hex = "0.4" urlencoding = "2.1" chrono = "0.4" roxmltree = "0.18"
补充说明
- 你之前没找到环境变量的原因:Fargate使用ECS任务元数据服务v2时,不再设置
AWS_CONTAINER_CREDENTIALS_RELATIVE_URI,而是通过文件和固定IP端点提供凭证信息。 - 若无需切换角色,直接使用元数据服务返回的凭证即可访问AWS服务,无需调用STS的AssumeRole接口。
内容的提问来源于stack exchange,提问作者Wolfgang Kuehn
相关产品推荐
相关产品推荐

