diff --git a/crates/openshell-sandbox/src/lib.rs b/crates/openshell-sandbox/src/lib.rs index 493e4d23..1d6ac275 100644 --- a/crates/openshell-sandbox/src/lib.rs +++ b/crates/openshell-sandbox/src/lib.rs @@ -202,7 +202,25 @@ pub async fn run_sandbox( std::collections::HashMap::new() }; - let (provider_env, secret_resolver) = SecretResolver::from_provider_env(provider_env); + // Use the placeholder mechanism only when the policy has at least one + // `tls: terminate` endpoint. Without TLS termination the proxy cannot + // intercept HTTPS traffic to rewrite credential placeholders in request + // headers, so placeholder values would reach upstream APIs verbatim and + // cause 401 errors. When no such endpoint exists, pass real credentials + // directly so API calls succeed. + let (provider_env, secret_resolver) = if policy.has_tls_terminate_endpoints { + SecretResolver::from_provider_env(provider_env) + } else { + if !provider_env.is_empty() { + warn!( + "Sandbox policy has no `tls: terminate` endpoints; \ + provider credentials are passed directly to the child process. \ + Add `protocol: rest` and `tls: terminate` to HTTPS endpoints \ + that use provider credentials to enable secure credential rewriting." + ); + } + (provider_env, None) + }; let secret_resolver = secret_resolver.map(Arc::new); // Create identity cache for SHA256 TOFU when OPA is active @@ -981,6 +999,9 @@ async fn load_policy( }, landlock: config.landlock, process: config.process, + // File-mode is a dev/operator override — assume the operator has + // configured `tls: terminate` where needed. + has_tls_terminate_endpoints: true, }; enrich_sandbox_baseline_paths(&mut policy); return Ok((policy, Some(Arc::new(engine)))); diff --git a/crates/openshell-sandbox/src/policy.rs b/crates/openshell-sandbox/src/policy.rs index 0827fa0d..c90e7872 100644 --- a/crates/openshell-sandbox/src/policy.rs +++ b/crates/openshell-sandbox/src/policy.rs @@ -17,6 +17,13 @@ pub struct SandboxPolicy { pub network: NetworkPolicy, pub landlock: LandlockPolicy, pub process: ProcessPolicy, + /// True when at least one network endpoint has `tls: terminate` configured. + /// + /// When false, the proxy cannot rewrite credential placeholder values in + /// HTTP headers (TLS MITM is required for that). Provider credentials are + /// passed directly to the child process instead of using the placeholder + /// mechanism so that API calls succeed. + pub has_tls_terminate_endpoints: bool, } #[derive(Debug, Clone)] @@ -106,6 +113,12 @@ impl TryFrom for SandboxPolicy { proxy: Some(ProxyPolicy { http_addr: None }), }; + let has_tls_terminate_endpoints = proto + .network_policies + .values() + .flat_map(|r| r.endpoints.iter()) + .any(|ep| ep.tls == "terminate"); + Ok(Self { version: proto.version, filesystem: proto @@ -115,6 +128,7 @@ impl TryFrom for SandboxPolicy { network, landlock: proto.landlock.map(LandlockPolicy::from).unwrap_or_default(), process: proto.process.map(ProcessPolicy::from).unwrap_or_default(), + has_tls_terminate_endpoints, }) } } diff --git a/crates/openshell-sandbox/src/process.rs b/crates/openshell-sandbox/src/process.rs index b93d125a..790c426f 100644 --- a/crates/openshell-sandbox/src/process.rs +++ b/crates/openshell-sandbox/src/process.rs @@ -524,6 +524,7 @@ mod tests { network: NetworkPolicy::default(), landlock: LandlockPolicy::default(), process, + has_tls_terminate_endpoints: false, } } diff --git a/crates/openshell-sandbox/testdata/sandbox-policy.yaml b/crates/openshell-sandbox/testdata/sandbox-policy.yaml index 76ad39b2..70e71177 100644 --- a/crates/openshell-sandbox/testdata/sandbox-policy.yaml +++ b/crates/openshell-sandbox/testdata/sandbox-policy.yaml @@ -33,7 +33,12 @@ network_policies: claude_code: name: claude_code endpoints: - - { host: api.anthropic.com, port: 443 } + - host: api.anthropic.com + port: 443 + protocol: rest + tls: terminate + enforcement: enforce + access: full - { host: statsig.anthropic.com, port: 443 } binaries: - { path: /usr/local/bin/claude }