From 75f0ba52906b760134018534052c599c289320b6 Mon Sep 17 00:00:00 2001 From: Eric Curtin Date: Sat, 21 Mar 2026 15:42:25 +0000 Subject: [PATCH] fix(sandbox): real creds when no tls:terminate Without a `tls: terminate` endpoint in the sandbox policy, the proxy cannot intercept HTTPS traffic to rewrite credential placeholders in request headers. Placeholder values reached upstream APIs verbatim, causing 401 errors and immediate process exit (e.g. `openshell sandbox create -- claude` exiting right after the Claude Code wizard appeared). N/A - Skip placeholder mechanism when policy has no `tls: terminate` endpoints; pass real provider credentials directly to child process - Emit a warning log directing operators to add `protocol: rest` and `tls: terminate` to HTTPS endpoints for secure credential rewriting - Add `has_tls_terminate_endpoints` field to `SandboxPolicy` populated from the proto network policy at startup - Update `testdata/sandbox-policy.yaml` to configure `api.anthropic.com:443` with correct `protocol`, `tls`, and `enforcement` fields - [x] `mise run pre-commit` passes - [x] Unit tests added/updated (285 pass, 0 failed) - [ ] E2E tests added/updated (if applicable) - [x] Follows Conventional Commits - [ ] Commits are signed off (DCO) - [ ] Architecture docs updated (if applicable) Signed-off-by: Eric Curtin --- crates/openshell-sandbox/src/lib.rs | 23 ++++++++++++++++++- crates/openshell-sandbox/src/policy.rs | 14 +++++++++++ crates/openshell-sandbox/src/process.rs | 1 + .../testdata/sandbox-policy.yaml | 7 +++++- 4 files changed, 43 insertions(+), 2 deletions(-) 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 }