[codex] close cross-platform workload identity gaps [ci changed_files]

This commit is contained in:
Cooper Gamble
2026-06-17 17:00:15 +00:00
parent 878da562ad
commit bb2b8ba0ae
3 changed files with 116 additions and 12 deletions
+70
View File
@@ -409,6 +409,76 @@ async fn rotating_workload_credential_denies_its_secret_directory() -> anyhow::R
Ok(())
}
#[cfg(unix)]
#[tokio::test]
async fn workload_credential_symlink_denies_external_target() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let secret_dir = codex_home.path().join("projected-secret");
let external_dir = codex_home.path().join("external-secret");
std::fs::create_dir_all(&secret_dir)?;
std::fs::create_dir_all(&external_dir)?;
let target = external_dir.join("token");
std::fs::write(&target, "subject-token")?;
let token_file = secret_dir.join("token");
std::os::unix::fs::symlink(&target, &token_file)?;
let cfg = ConfigToml {
workload_identity: Some(WorkloadIdentityConfig {
identity_provider_id: "idp_example".to_string(),
identity_provider_mapping_id: "idpm_example".to_string(),
audience: "openai-audience".to_string(),
token_url: "https://auth.openai.com/oauth/token".to_string(),
credential_source: CredentialSourceConfig::File {
path: token_file.clone(),
},
}),
..Default::default()
};
let config = Config::load_from_base_config_with_overrides(
cfg,
ConfigOverrides::default(),
codex_home.abs(),
)
.await?;
let file_system_policy = config.permissions.file_system_sandbox_policy();
let matcher = ReadDenyMatcher::new(&file_system_policy, config.cwd.as_path())
.expect("workload credential should install a deny-read matcher");
assert!(matcher.is_read_denied(&token_file));
assert!(matcher.is_read_denied(&target));
Ok(())
}
#[cfg(target_os = "windows")]
#[tokio::test]
async fn workload_identity_enables_windows_restricted_token_sandbox() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let cfg = ConfigToml {
workload_identity: Some(WorkloadIdentityConfig {
identity_provider_id: "idp_example".to_string(),
identity_provider_mapping_id: "idpm_example".to_string(),
audience: "openai-audience".to_string(),
token_url: "https://auth.openai.com/oauth/token".to_string(),
credential_source: CredentialSourceConfig::Environment {
variable: "WORKLOAD_IDENTITY_TOKEN".to_string(),
},
}),
..Default::default()
};
let config = Config::load_from_base_config_with_overrides(
cfg,
ConfigOverrides::default(),
codex_home.abs(),
)
.await?;
assert_eq!(
config.permissions.windows_sandbox_mode,
Some(WindowsSandboxModeToml::Unelevated)
);
Ok(())
}
async fn derive_legacy_sandbox_policy_for_test(
cfg: &ConfigToml,
sandbox_mode_override: Option<SandboxMode>,
+41 -12
View File
@@ -2353,7 +2353,7 @@ fn apply_workload_identity_filesystem_constraints(
.credential_source
.credential_file_paths()
.into_iter()
.filter_map(workload_identity_credential_deny_path)
.flat_map(workload_identity_credential_deny_paths)
.map(|path| codex_protocol::permissions::FileSystemSandboxEntry {
path: codex_protocol::permissions::FileSystemPath::Path { path },
access: codex_protocol::permissions::FileSystemAccessMode::Deny,
@@ -2367,23 +2367,38 @@ fn apply_workload_identity_filesystem_constraints(
.preserve_deny_read_restrictions_from(&FileSystemSandboxPolicy::restricted(entries));
}
fn workload_identity_credential_deny_path(path: PathBuf) -> Option<AbsolutePathBuf> {
let path = AbsolutePathBuf::relative_to_current_dir(path).ok()?;
fn workload_identity_credential_deny_paths(path: PathBuf) -> Vec<AbsolutePathBuf> {
let Ok(path) = AbsolutePathBuf::relative_to_current_dir(path) else {
return Vec::new();
};
let Some(parent) = path.as_path().parent() else {
return Some(path);
return vec![path];
};
let parent = std::fs::canonicalize(parent).unwrap_or_else(|_| parent.to_path_buf());
// Projected service-account tokens are symlinks whose targets rotate. Mask
// the containing secret directory so a rotation cannot expose a new target.
let deny_path = if std::fs::symlink_metadata(path.as_path())
.is_ok_and(|metadata| metadata.file_type().is_symlink())
let is_symlink = std::fs::symlink_metadata(path.as_path())
.is_ok_and(|metadata| metadata.file_type().is_symlink());
if !is_symlink {
return path
.as_path()
.file_name()
.and_then(|file_name| AbsolutePathBuf::from_absolute_path(parent.join(file_name)).ok())
.into_iter()
.collect();
}
let mut deny_paths = AbsolutePathBuf::from_absolute_path(&parent)
.into_iter()
.collect::<Vec<_>>();
if let Ok(target) = std::fs::canonicalize(path.as_path())
&& !target.starts_with(&parent)
&& let Ok(target) = AbsolutePathBuf::from_absolute_path(target)
{
parent
} else {
parent.join(path.as_path().file_name()?)
};
AbsolutePathBuf::from_absolute_path(deny_path).ok()
deny_paths.push(target);
}
deny_paths
}
/// Optional overrides for user configuration (e.g., from CLI flags).
@@ -2868,7 +2883,12 @@ impl Config {
)?;
let respect_system_proxy = features.enabled(Feature::RespectSystemProxy);
let enable_network_proxy = features.enabled(Feature::NetworkProxy);
let configured_windows_sandbox_mode = resolve_windows_sandbox_mode(&cfg);
// A restricted child token is the Windows boundary that prevents model-controlled
// processes from inspecting WIF credentials held by the parent process.
let configured_windows_sandbox_mode = resolve_windows_sandbox_mode(&cfg).or_else(|| {
(cfg!(target_os = "windows") && cfg.workload_identity.is_some())
.then_some(WindowsSandboxModeToml::Unelevated)
});
// Keep the configured mode separate so a requirement-constrained mode
// does not look like it was explicitly selected in config.
let selected_windows_sandbox_mode = configured_windows_sandbox_mode.or_else(|| {
@@ -2885,6 +2905,15 @@ impl Config {
&mut startup_warnings,
)?;
let effective_windows_sandbox_mode = *constrained_windows_sandbox_mode.get();
if cfg!(target_os = "windows")
&& cfg.workload_identity.is_some()
&& effective_windows_sandbox_mode.is_none()
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"`workload_identity` requires the Windows restricted-token sandbox",
));
}
let windows_sandbox_mode = if constrained_windows_sandbox_mode.source.is_some() {
effective_windows_sandbox_mode
} else {
@@ -13,6 +13,11 @@ pub(super) struct WorkloadIdentityExternalAuth {
impl WorkloadIdentityExternalAuth {
pub(super) fn new(client: ConfiguredWorkloadIdentityClient) -> Self {
#[cfg(target_os = "windows")]
// Core config forces WIF sessions onto the Windows restricted-token sandbox, which keeps
// model-controlled child processes from opening the parent process.
let process_isolation_error = None;
#[cfg(not(target_os = "windows"))]
let process_isolation_error = codex_process_hardening::disable_process_inspection()
.err()
.map(|error| format!("workload identity process isolation failed: {error}"));