fix(bedrock): pass AWS credential-chain env into the workflow worker

The workflow worker subprocess clears its environment and copies only an
allowlist, so Bedrock SigV4 — which re-resolves credentials from the ambient
AWS chain per request rather than from a stored secret — fell through to the
default ~/.aws profile and signed as the wrong principal. SigV4 worked via
`model test` (server-direct) but not via `fabro run` (scrubbed worker).

Add the AWS credential-chain inputs (static keys, session token, bearer key,
profile/region selectors, and the web-identity/ECS role vars) to the worker
allowlist so env/profile/SSO/IRSA all resolve in the worker. Pass the inputs,
not a launch-time snapshot, so STS/SSO/IRSA sessions still refresh. The
fail-closed test now proves AWS identity vars cross while a generic secret
still does not.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott Werner 2026-06-14 22:16:05 -04:00
parent 5a7ecf57e1
commit bfed47ea92
2 changed files with 59 additions and 0 deletions

View file

@ -17,6 +17,31 @@ const WORKER_ENV_ALLOWLIST: &[&str] = &[
EnvVars::NO_COLOR,
EnvVars::CLICOLOR,
EnvVars::CLICOLOR_FORCE,
// AWS credential-chain inputs for the Bedrock provider. Other providers'
// secrets reach the worker through the server vault (read via FABRO_HOME),
// but Bedrock SigV4 has no stored secret — it re-resolves from the ambient
// AWS chain on every request so STS/SSO/IRSA sessions can refresh, which
// means the chain's *inputs* must survive `env_clear()` in the worker, not
// a snapshot taken at launch. We pass the identity surface only (static
// keys, session token, the Bedrock bearer key, profile/region selectors,
// and the web-identity/ECS role vars); HOME already carries the shared
// `~/.aws` config + SSO cache. Endpoint/metadata overrides
// (AWS_ENDPOINT_*, AWS_METADATA_ENDPOINT, AWS_IMDSV1_FALLBACK) are
// deliberately excluded — they belong to the server's S3 path, not to the
// worker's outbound model calls.
EnvVars::AWS_ACCESS_KEY_ID,
EnvVars::AWS_SECRET_ACCESS_KEY,
EnvVars::AWS_SESSION_TOKEN,
EnvVars::AWS_BEARER_TOKEN_BEDROCK,
EnvVars::AWS_PROFILE,
EnvVars::AWS_REGION,
EnvVars::AWS_DEFAULT_REGION,
EnvVars::AWS_ROLE_ARN,
EnvVars::AWS_ROLE_SESSION_NAME,
EnvVars::AWS_WEB_IDENTITY_TOKEN_FILE,
EnvVars::AWS_CONTAINER_CREDENTIALS_RELATIVE_URI,
EnvVars::AWS_CONTAINER_CREDENTIALS_FULL_URI,
EnvVars::AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE,
];
const RENDER_GRAPH_ENV_ALLOWLIST: &[&str] = &[EnvVars::PATH, EnvVars::HOME, EnvVars::TMPDIR];
@ -91,6 +116,11 @@ mod tests {
("NO_COLOR".to_string(), "1".to_string()),
("CLICOLOR".to_string(), "0".to_string()),
("CLICOLOR_FORCE".to_string(), "1".to_string()),
("AWS_ACCESS_KEY_ID".to_string(), "AKIAEXAMPLE".to_string()),
("AWS_SECRET_ACCESS_KEY".to_string(), "secret".to_string()),
("AWS_SESSION_TOKEN".to_string(), "session".to_string()),
("AWS_BEARER_TOKEN_BEDROCK".to_string(), "bearer".to_string()),
("AWS_REGION".to_string(), "us-east-2".to_string()),
("SESSION_SECRET".to_string(), "leak".to_string()),
("FABRO_JWT_PRIVATE_KEY".to_string(), "leak".to_string()),
("FABRO_JWT_PUBLIC_KEY".to_string(), "leak".to_string()),
@ -122,6 +152,29 @@ mod tests {
assert_eq!(actual.get("NO_COLOR").map(String::as_str), Some("1"));
assert_eq!(actual.get("CLICOLOR").map(String::as_str), Some("0"));
assert_eq!(actual.get("CLICOLOR_FORCE").map(String::as_str), Some("1"));
// Bedrock SigV4 chain inputs cross into the worker so it can re-resolve
// credentials per request; a generic secret with no allowlist entry
// still does not.
assert_eq!(
actual.get("AWS_ACCESS_KEY_ID").map(String::as_str),
Some("AKIAEXAMPLE")
);
assert_eq!(
actual.get("AWS_SECRET_ACCESS_KEY").map(String::as_str),
Some("secret")
);
assert_eq!(
actual.get("AWS_SESSION_TOKEN").map(String::as_str),
Some("session")
);
assert_eq!(
actual.get("AWS_BEARER_TOKEN_BEDROCK").map(String::as_str),
Some("bearer")
);
assert_eq!(
actual.get("AWS_REGION").map(String::as_str),
Some("us-east-2")
);
assert!(!actual.contains_key("FABRO_LOG_DESTINATION"));
assert_eq!(
actual.get("FABRO_DEV_TOKEN").map(String::as_str),

View file

@ -82,11 +82,14 @@ impl EnvVars {
"AWS_CONTAINER_CREDENTIALS_FULL_URI";
pub const AWS_CONTAINER_CREDENTIALS_RELATIVE_URI: &'static str =
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI";
pub const AWS_DEFAULT_REGION: &'static str = "AWS_DEFAULT_REGION";
pub const AWS_ENDPOINT: &'static str = "AWS_ENDPOINT";
pub const AWS_ENDPOINT_URL_S3: &'static str = "AWS_ENDPOINT_URL_S3";
pub const AWS_ENDPOINT_URL_STS: &'static str = "AWS_ENDPOINT_URL_STS";
pub const AWS_IMDSV1_FALLBACK: &'static str = "AWS_IMDSV1_FALLBACK";
pub const AWS_METADATA_ENDPOINT: &'static str = "AWS_METADATA_ENDPOINT";
pub const AWS_PROFILE: &'static str = "AWS_PROFILE";
pub const AWS_REGION: &'static str = "AWS_REGION";
pub const AWS_ROLE_ARN: &'static str = "AWS_ROLE_ARN";
pub const AWS_ROLE_SESSION_NAME: &'static str = "AWS_ROLE_SESSION_NAME";
pub const AWS_SECRET_ACCESS_KEY: &'static str = "AWS_SECRET_ACCESS_KEY";
@ -214,11 +217,14 @@ mod tests {
EnvVars::AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE,
EnvVars::AWS_CONTAINER_CREDENTIALS_FULL_URI,
EnvVars::AWS_CONTAINER_CREDENTIALS_RELATIVE_URI,
EnvVars::AWS_DEFAULT_REGION,
EnvVars::AWS_ENDPOINT,
EnvVars::AWS_ENDPOINT_URL_S3,
EnvVars::AWS_ENDPOINT_URL_STS,
EnvVars::AWS_IMDSV1_FALLBACK,
EnvVars::AWS_METADATA_ENDPOINT,
EnvVars::AWS_PROFILE,
EnvVars::AWS_REGION,
EnvVars::AWS_ROLE_ARN,
EnvVars::AWS_ROLE_SESSION_NAME,
EnvVars::AWS_SECRET_ACCESS_KEY,