mod cache_manager;
mod constants;
mod error;
mod parse;
mod prefetch;
mod server;
mod utils;
use std::net::SocketAddr;
use log::{error, info};
use tokio::net::TcpListener;
use tokio_util::sync::CancellationToken;
use aws_workload_credentials_provider_common::config::types::ValidatedConfig;
#[cfg(unix)]
use aws_workload_credentials_provider_common::shutdown_signal;
use constants::VERSION;
use server::Server;
use utils::get_token;
pub async fn sm_workload(
config: ValidatedConfig,
token: CancellationToken,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let listener = init_listener(&config).await?;
serve(listener, &config, token).await
}
async fn serve(
listener: TcpListener,
config: &ValidatedConfig,
token: CancellationToken,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let addr = listener.local_addr()?;
let svr = Server::new(listener, config)
.await
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { format!("{e}").into() })?;
if config.secrets_manager.prefetch.is_enabled() {
info!("Pre-fetch enabled, starting background task");
prefetch::start_prefetch_task(svr.cache_manager(), config.secrets_manager.clone());
}
let start_msg = format!(
"Secrets Manager Provider/{} listening on http://{}",
VERSION.unwrap_or("0.0.0"),
addr
);
println!("{start_msg}");
info!("{start_msg}");
tokio::select! {
_ = async {
loop {
if let Err(e) = svr.serve_request().await {
error!("Could not accept connection: {e:?}");
}
}
} => {},
_ = token.cancelled() => {
info!("Secrets Manager workload shutting down");
}
}
Ok(())
}
#[cfg(unix)]
pub fn run_sm(config: ValidatedConfig) -> Result<(), Box<dyn std::error::Error>> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let cancel = CancellationToken::new();
let token = cancel.clone();
tokio::spawn(async move {
shutdown_signal().await;
token.cancel();
});
sm_workload(config, cancel)
.await
.map_err(|e| -> Box<dyn std::error::Error> { e })
})
}
async fn init_listener(
config: &ValidatedConfig,
) -> Result<TcpListener, Box<dyn std::error::Error + Send + Sync>> {
if let Err(err) = get_token(&config.secrets_manager.security.ssrf_env_variables) {
let msg = format!(
"Could not read SSRF token variable(s) {:?}: {err}",
config.secrets_manager.security.ssrf_env_variables
);
return Err(msg.into());
}
let addr: SocketAddr = ([127, 0, 0, 1], config.secrets_manager.http_port).into();
let listener =
TcpListener::bind(addr)
.await
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
format!("Could not bind to {addr}: {e}").into()
})?;
Ok(listener)
}
#[cfg(test)]
mod tests {
use super::*;
use aws_sdk_secretsmanager as secretsmanager;
use aws_workload_credentials_provider_common::config::types::{
LoggingConfig, SecretsManagerConfig, SecurityConfig,
};
use aws_workload_credentials_provider_common::config::validator::ConfigValidator;
use bytes::Bytes;
use cache_manager::tests::{
set_client, timeout_client, DEFAULT_LABEL, DEFAULT_VERSION, FAKE_ARN,
};
use http_body_util::{BodyExt, Empty};
use hyper::header::{HeaderName, HeaderValue};
use hyper::{client, Request, StatusCode};
use hyper_util::rt::TokioIo;
use serde_json::Value;
use std::net::SocketAddr;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::sync::{mpsc, Arc, Mutex};
use std::time::Duration;
use std::{fs, thread};
use tokio::net::TcpStream;
use tokio::task::JoinSet;
#[cfg(unix)]
use utils::tests::set_test_var;
use utils::tests::{tmpfile_name, CleanUp};
async fn run_request(req: &str) -> (StatusCode, Bytes) {
run_requests_with_verb(vec![("GET", req)])
.await
.expect("request failed")
.pop()
.unwrap()
}
async fn run_requests_with_verb(
req_vec: Vec<(&str, &str)>,
) -> Result<Vec<(StatusCode, Bytes)>, Box<dyn std::error::Error>> {
run_requests_with_headers(req_vec, vec![("X-Aws-Parameters-Secrets-Token", "xyzzy")]).await
}
async fn run_requests_with_headers(
req_vec: Vec<(&str, &str)>,
headers: Vec<(&str, &str)>,
) -> Result<Vec<(StatusCode, Bytes)>, Box<dyn std::error::Error>> {
run_requests_with_client(req_vec, headers, None).await
}
async fn run_timeout_request(req: &str) -> (StatusCode, Bytes) {
run_requests_with_client(
vec![("GET", req)],
vec![("X-Aws-Parameters-Secrets-Token", "xyzzy")],
Some(timeout_client()),
)
.await
.expect("request failed")
.pop()
.unwrap()
}
async fn run_requests_with_client(
req_vec: Vec<(&str, &str)>,
headers: Vec<(&str, &str)>,
opt_client: Option<secretsmanager::Client>,
) -> Result<Vec<(StatusCode, Bytes)>, Box<dyn std::error::Error>> {
let cfg = ValidatedConfig {
logging: LoggingConfig::default(),
secrets_manager: SecretsManagerConfig {
http_port: 0,
max_conn: 1,
..Default::default()
},
acm: None,
};
let (tx_addr, rx_addr) = mpsc::channel(); let token = CancellationToken::new();
let token_for_thread = token.clone();
let thr = thread::Builder::new().spawn(move || {
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_all()
.build()
.unwrap();
rt.block_on(async move {
if let Some(client) = opt_client {
set_client(client);
}
let listener = init_listener(&cfg).await.expect("could not bind listener");
let addr = listener.local_addr().expect("could not get local addr");
tx_addr.send(addr).expect("could not send address");
serve(listener, &cfg, token_for_thread)
.await
.expect("could not run server");
})
})?;
let addr = rx_addr.recv()?;
let mut join_set = JoinSet::new();
let send_cnt = req_vec.len();
let mut idx = 0;
let responses = Arc::new(Mutex::new(Vec::new()));
for (meth, query) in req_vec.clone() {
let stream = TcpStream::connect(addr)
.await
.expect("could not setup client stream");
let io = TokioIo::new(stream);
let (mut sender, conn) = client::conn::http1::handshake(io)
.await
.expect("could not setup client");
tokio::spawn(async move {
if let Err(e) = conn.await {
panic!("Error in connection: {}", e);
}
});
let mut req = Request::builder()
.uri(query)
.method(meth)
.body(Empty::<Bytes>::new())
.expect("could not build request");
for (header, header_val) in headers.clone() {
req.headers_mut().insert(
HeaderName::from_lowercase(header.to_lowercase().as_bytes())?,
HeaderValue::from_str(header_val)?,
);
}
let rsp_vec = responses.clone();
join_set.spawn(async move {
let rsp = match sender.send_request(req).await {
Ok(x) => x,
Err(h_err) if h_err.is_incomplete_message() => {
rsp_vec.lock().expect("lock poisoned").push((
idx,
StatusCode::GATEWAY_TIMEOUT,
Bytes::new(),
));
return;
}
_ => panic!("unknown error sending request"),
};
let status = rsp.status();
let data = rsp
.into_body()
.collect()
.await
.expect("can not read body")
.to_bytes();
rsp_vec
.lock()
.expect("lock poisoned")
.push((idx, status, data));
});
idx += 1;
if idx < send_cnt {
tokio::time::sleep(Duration::from_secs(4)).await;
}
}
while let Some(res) = join_set.join_next().await {
res.expect("task failed");
}
token.cancel();
if let Err(msg) = thr.join() {
panic!("server failed: {:?}", msg);
}
let mut rsp_vec = responses.clone().lock().expect("lock poisoned").to_vec();
rsp_vec.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap());
Ok(rsp_vec
.iter()
.map(|x| (x.1, x.2.clone()))
.collect::<Vec<_>>())
}
fn validate_response(name: &str, body: Bytes) {
validate_response_extra(name, DEFAULT_VERSION, vec![DEFAULT_LABEL], body);
}
fn validate_response_extra(name: &str, version: &str, labels: Vec<&str>, body: Bytes) {
let map: serde_json::Map<String, Value> = serde_json::from_slice(&body).unwrap();
let fake_arn = FAKE_ARN.replace("{{name}}", name);
assert_eq!(map.get("Name").unwrap(), name);
assert_eq!(map.get("ARN").unwrap(), &fake_arn);
assert_eq!(map.get("VersionId").unwrap(), version);
if !name.contains("REFRESHNOW") {
assert_eq!(map.get("SecretString").unwrap(), "hunter2");
}
assert_eq!(map.get("CreatedDate").unwrap(), "1569534789.046");
assert_eq!(
map.get("VersionStages").unwrap().as_array().unwrap(),
&labels
);
}
fn validate_err(err_code: &str, msg: &str, body: Bytes) {
let map: serde_json::Map<String, Value> = serde_json::from_slice(&body).unwrap();
assert_eq!(map.get("__type").unwrap(), err_code);
if !msg.is_empty() && err_code != "InternalFailure" {
assert_eq!(map.get("message").unwrap(), msg);
}
}
#[tokio::test]
async fn no_token_env() {
let cfg = ValidatedConfig {
logging: LoggingConfig::default(),
secrets_manager: SecretsManagerConfig {
http_port: 0,
security: SecurityConfig {
ssrf_env_variables: vec!["FAIL_TOKEN".to_string()],
..Default::default()
},
..Default::default()
},
acm: None,
};
let err = init_listener(&cfg)
.await
.expect_err("expected init_listener to fail");
assert_eq!(
err.to_string(),
"Could not read SSRF token variable(s) [\"FAIL_TOKEN\"]: environment variable not found"
);
}
#[cfg(unix)]
#[tokio::test]
async fn bad_token_file() {
let tmpfile = tmpfile_name("bad_token_file.toml");
let _cleanup = CleanUp {
file: Some(&tmpfile),
};
fs::write(&tmpfile, "xyzzy").expect("could not write");
fs::set_permissions(&tmpfile, fs::Permissions::from_mode(0o333))
.expect("could not set perms"); let file = Box::new(format!("file://{tmpfile}"));
set_test_var("AWS_TOKEN", Box::leak(file));
let cfg = ConfigValidator::new()
.validate(None)
.expect("could not validate default config");
let err = init_listener(&cfg)
.await
.expect_err("expected init_listener to fail");
assert_eq!(
err.to_string(),
"Could not read SSRF token variable(s) [\"AWS_TOKEN\", \"AWS_SESSION_TOKEN\", \"AWS_CONTAINER_AUTHORIZATION_TOKEN\"]: Permission denied (os error 13)"
);
}
#[tokio::test]
async fn port_in_use() {
let addr: SocketAddr = ([127, 0, 0, 1], 0).into();
let listener: TcpListener = TcpListener::bind(addr)
.await
.expect("Could not bind to port");
let port = listener.local_addr().expect("can not find port").port();
let cfg = ValidatedConfig {
logging: LoggingConfig::default(),
secrets_manager: SecretsManagerConfig {
http_port: port,
..Default::default()
},
acm: None,
};
let err = init_listener(&cfg)
.await
.expect_err("expected init_listener to fail");
let msg = err.to_string();
#[cfg(unix)]
assert!(
msg.contains("Address already in use"),
"unexpected error: {msg}"
);
#[cfg(windows)]
assert!(
msg.contains(
"Only one usage of each socket address (protocol/network address/port) is normally permitted."
),
"unexpected error: {msg}"
);
}
#[tokio::test]
async fn ping_req() {
let (status, body) = run_request("/ping").await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body, "healthy");
}
#[tokio::test]
async fn ping_no_token() {
let (status, body) = run_requests_with_headers(vec![("GET", "/ping")], vec![])
.await
.expect("request failed")
.pop()
.unwrap();
assert_eq!(status, StatusCode::OK);
assert_eq!(body, "healthy");
}
#[tokio::test]
async fn pong_req() {
let (status, _) = run_request("/pong").await;
assert_eq!(status, StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn missing_id() {
let (status, _) = run_request("/secretsmanager/get").await;
assert_eq!(status, StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn missing_path_id() {
let (status, _) = run_request("/v1/").await;
assert_eq!(status, StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn bad_query_parameter() {
let (status, _) = run_request(
"/secretsmanager/get?secretId=MyTest&versionStage=AWSPENDING&abc=XXXXXXXXXXXX",
)
.await;
assert_eq!(status, StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn path_bad_query_parameter() {
let (status, _) = run_request("/v1/MyTest?versionStage=AWSPENDING&abc=XXXXXXXXXXXX").await;
assert_eq!(status, StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn missing_query_parameter() {
let (status, _) = run_request("/secretsmanager/get?versionStage=AWSPENDING").await;
assert_eq!(status, StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn basic_success() {
let (status, body) = run_request("/secretsmanager/get?secretId=MyTest").await;
assert_eq!(status, StatusCode::OK);
validate_response("MyTest", body);
}
#[tokio::test]
async fn basic_refresh_success() {
let (status, body) = run_request("/secretsmanager/get?secretId=MyTest&refreshNow=1").await;
assert_eq!(status, StatusCode::OK);
validate_response("MyTest", body);
}
#[tokio::test]
async fn pending_success() {
let req = "/secretsmanager/get?secretId=MyTest&versionStage=AWSPENDING".to_string();
let (status, body) = run_request(&req).await;
assert_eq!(status, StatusCode::OK);
validate_response_extra("MyTest", DEFAULT_VERSION, vec!["AWSPENDING"], body);
}
#[tokio::test]
async fn version_success() {
let ver = "11111";
let req = format!("/secretsmanager/get?secretId=MyTest&versionId={ver}");
let (status, body) = run_request(&req).await;
assert_eq!(status, StatusCode::OK);
validate_response_extra("MyTest", ver, vec![DEFAULT_LABEL], body);
}
#[tokio::test]
async fn all_args_success() {
let ver = "000000000000";
let req =
format!("/secretsmanager/get?secretId=MyTest&versionStage=AWSPENDING&versionId={ver}&refreshNow=true");
let (status, body) = run_request(&req).await;
assert_eq!(status, StatusCode::OK);
validate_response_extra("MyTest", ver, vec!["AWSPENDING"], body);
}
#[tokio::test]
async fn access_denied_test() {
let (status, body) = run_request("/secretsmanager/get?secretId=KMSACCESSDENIEDTest").await;
assert_eq!(status, StatusCode::BAD_REQUEST);
validate_err(
"AccessDeniedException",
"Access to KMS is not allowed",
body,
);
}
#[tokio::test]
async fn other_error_test() {
let (status, body) = run_request("/secretsmanager/get?secretId=OTHERERRORTest").await;
assert_eq!(status, StatusCode::BAD_REQUEST);
validate_err(
"InvalidSignatureException",
"The request signature we calculated does not match ...",
body,
);
}
#[tokio::test]
async fn refresh_now_test() {
let responses = run_requests_with_client(
vec![
("GET", "/secretsmanager/get?secretId=REFRESHNOWtestsecret"),
("GET", "/secretsmanager/get?secretId=REFRESHNOWtestsecret"),
(
"GET",
"/secretsmanager/get?secretId=REFRESHNOWtestsecret&refreshNow=true",
),
],
vec![("X-Aws-Parameters-Secrets-Token", "xyzzy")],
None,
)
.await
.unwrap();
let mut secret_strings = Vec::new();
for (status, body) in responses {
assert_eq!(status, StatusCode::OK);
let map: serde_json::Map<String, Value> = serde_json::from_slice(&body).unwrap();
let secret_string = map.get("SecretString").unwrap().to_string();
secret_strings.insert(0, secret_string)
}
assert_ne!(secret_strings[1], secret_strings[2]);
assert_eq!(secret_strings[0], secret_strings[1]);
}
#[tokio::test]
async fn path_success() {
let (status, body) = run_requests_with_headers(
vec![("GET", "/v1/MyTest")],
vec![("X-Vault-Token", "xyzzy")],
)
.await
.expect("request failed")
.pop()
.unwrap();
assert_eq!(status, StatusCode::OK);
validate_response("MyTest", body);
}
#[tokio::test]
async fn path_pending_success() {
let req = "/v1/My/Test?versionStage=AWSPENDING";
let (status, body) = run_request(req).await;
assert_eq!(status, StatusCode::OK);
validate_response_extra("My/Test", DEFAULT_VERSION, vec!["AWSPENDING"], body);
}
#[tokio::test]
async fn path_refresh_success() {
let req = "/v1/My/Test?versionStage=AWSPENDING&refreshNow=0";
let (status, body) = run_request(&req).await;
assert_eq!(status, StatusCode::OK);
validate_response_extra("My/Test", DEFAULT_VERSION, vec!["AWSPENDING"], body);
}
#[tokio::test]
async fn path_version_success() {
let ver = "11111";
let req = format!("/v1/My/Test?versionId={ver}");
let (status, body) = run_request(&req).await;
assert_eq!(status, StatusCode::OK);
validate_response_extra("My/Test", ver, vec![DEFAULT_LABEL], body);
}
#[tokio::test]
async fn path_all_args_success() {
let ver = "000000000000";
let req = format!("/v1/My/Test?versionStage=AWSPENDING&versionId={ver}&refreshNow=true");
let (status, body) = run_request(&req).await;
assert_eq!(status, StatusCode::OK);
validate_response_extra("My/Test", ver, vec!["AWSPENDING"], body);
}
#[tokio::test]
async fn no_token_fail() {
let (status, _) =
run_requests_with_headers(vec![("GET", "/secretsmanager/get?secretId=MyTest")], vec![])
.await
.expect("request failed")
.pop()
.unwrap();
assert_eq!(status, StatusCode::FORBIDDEN);
}
#[tokio::test]
async fn path_no_token_fail() {
let (status, _) = run_requests_with_headers(vec![("GET", "/v1/MyTest")], vec![])
.await
.expect("request failed")
.pop()
.unwrap();
assert_eq!(status, StatusCode::FORBIDDEN);
}
#[tokio::test]
async fn bad_token() {
let (status, _) = run_requests_with_headers(
vec![("GET", "/secretsmanager/get?secretId=MyTest")],
vec![("X-Vault-Token", "click slipers")],
)
.await
.expect("request failed")
.pop()
.unwrap();
assert_eq!(status, StatusCode::FORBIDDEN);
}
#[tokio::test]
async fn xff_fail() {
let (status, _) = run_requests_with_headers(
vec![("GET", "/secretsmanager/get?secretId=MyTest")],
vec![
("X-Vault-Token", "xyzzy"),
("X-Forwarded-For", "54.239.28.85"),
],
)
.await
.expect("request failed")
.pop()
.unwrap();
assert_eq!(status, StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn max_conn_test() {
let reqs = vec![
("GET", "/secretsmanager/get?secretId=SleepyTest_6"), ("GET", "/secretsmanager/get?secretId=MyTest"), ("GET", "/secretsmanager/get?secretId=MyTest"), ];
let mut rsp = run_requests_with_verb(reqs).await.expect("request failed");
assert_eq!(rsp.len(), 3);
let (status, body) = rsp.pop().unwrap();
assert_eq!(status, StatusCode::OK);
validate_response("SleepyTest_6", body);
let (status, _) = rsp.pop().unwrap();
assert_eq!(status, StatusCode::TOO_MANY_REQUESTS);
let (status, body) = rsp.pop().unwrap();
assert_eq!(status, StatusCode::OK);
validate_response("MyTest", body);
}
#[tokio::test]
async fn ping_max_conn() {
let reqs = vec![
("GET", "/secretsmanager/get?secretId=SleepyTest_6"), ("GET", "/ping"), ];
let mut rsp = run_requests_with_verb(reqs).await.expect("request failed");
assert_eq!(rsp.len(), 2);
let (status, body) = rsp.pop().unwrap();
assert_eq!(status, StatusCode::OK);
validate_response("SleepyTest_6", body);
let (status, body) = rsp.pop().unwrap();
assert_eq!(status, StatusCode::OK);
assert_eq!(body, "healthy");
}
#[tokio::test]
async fn timeout_test() {
let (status, _) = run_timeout_request(&format!(
"/secretsmanager/get?secretId=SleepyTest_{}",
u64::MAX
))
.await;
assert_eq!(status, StatusCode::GATEWAY_TIMEOUT);
}
#[tokio::test]
async fn get_only() {
for verb in [
"POST", "PUT", "PATCH", "DELETE", "HEAD", "CONNECT", "OPTIONS", "TRACE",
] {
let (status, _) =
run_requests_with_verb(vec![(verb, "/secretsmanager/get?secretId=MyTest")])
.await
.expect("request failed")
.pop()
.unwrap();
assert_eq!(status, StatusCode::METHOD_NOT_ALLOWED);
let (status, _) = run_requests_with_verb(vec![(verb, "/v1/MyTest")])
.await
.expect("request failed")
.pop()
.unwrap();
assert_eq!(status, StatusCode::METHOD_NOT_ALLOWED);
let (status, _) = run_requests_with_verb(vec![(verb, "/ping")])
.await
.expect("request failed")
.pop()
.unwrap();
assert_eq!(status, StatusCode::METHOD_NOT_ALLOWED);
}
}
#[tokio::test]
async fn role_arn_query_success() {
let role = "arn:aws:iam::123456789012:role/TestRole";
let (status, body) = run_request(&format!(
"/secretsmanager/get?secretId=MyTest&roleArn={role}"
))
.await;
assert_eq!(status, StatusCode::OK);
validate_response("MyTest", body);
}
#[tokio::test]
async fn role_arn_path_success() {
let role = "arn:aws:iam::123456789012:role/TestRole";
let (status, body) = run_request(&format!("/v1/MyTest?roleArn={role}")).await;
assert_eq!(status, StatusCode::OK);
validate_response("MyTest", body);
}
#[tokio::test]
async fn role_arn_invalid() {
let (status, body) =
run_request("/secretsmanager/get?secretId=MyTest&roleArn=not-a-valid-arn").await;
assert_eq!(status, StatusCode::BAD_REQUEST);
assert_eq!(
body,
"invalid roleArn format, expected arn:<partition>:iam::<account>:role/<name>"
);
}
#[tokio::test]
async fn role_arn_with_all_params() {
let role = "arn:aws:iam::123456789012:role/TestRole";
let ver = "11111";
let (status, body) = run_request(
&format!("/secretsmanager/get?secretId=MyTest&versionId={ver}&versionStage=AWSPENDING&refreshNow=true&roleArn={role}"),
)
.await;
assert_eq!(status, StatusCode::OK);
validate_response_extra("MyTest", ver, vec!["AWSPENDING"], body);
}
}