Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ terminal-colorsaurus = "1.0"
miette = { version = "7", features = ["fancy"] }
thiserror = "2"

# Windows platform APIs (ETW/TDH audit consumer in openshell-driver-mxc; Windows-only)
windows = { version = "0.62", features = ["Wdk_System_Threading", "Win32_Foundation", "Win32_System_Diagnostics_Etw", "Win32_System_Time"] }
# Windows platform APIs (MXC audit and host-proxy process identity; Windows-only)
windows = { version = "0.62", features = ["Wdk_System_Threading", "Win32_Foundation", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_System_Diagnostics_Etw", "Win32_System_Threading", "Win32_System_Time"] }
anyhow = "1"

# Logging/Tracing
Expand Down
7 changes: 7 additions & 0 deletions architecture/security-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,13 @@ middleware, token grants, credential rewriting, policy-generation checks, and
the HTTP relay have succeeded so a later denial cannot coexist with an allowed
record for the same request.

Each Windows MXC sandbox owns a separate host proxy. That proxy carries an
immutable per-sandbox OCSF context so proxy lifecycle events and top-level
CONNECT/forward decisions use the correct `container.uid` and `container.name`
even when one gateway serves multiple sandboxes concurrently. The process-wide
sandbox context is only suitable for the one-supervisor-per-sandbox runtime
model.

Never log secrets, credentials, bearer tokens, or query parameters in OCSF
messages. OCSF JSONL output may be shipped to external systems.
The gateway-local OCSF JSONL file sink is restricted to the Windows/MXC path
Expand Down
9 changes: 0 additions & 9 deletions crates/openshell-driver-mxc/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -508,14 +508,6 @@ fn allocate_sandbox_proxy_addr(
const MINIMAL_WINDOWS_BOOTSTRAP_ENV: [&str; 5] =
["SYSTEMROOT", "WINDIR", "PATH", "COMSPEC", "LOCALAPPDATA"];

fn host_proxy_binary_path(config: &MxcSandboxConfig) -> PathBuf {
config
.command
.first()
.filter(|command| !command.trim().is_empty())
.map_or_else(|| PathBuf::from("mxc-agent"), PathBuf::from)
}

const TLS_ENV_KEYS: [&str; 6] = [
"NODE_EXTRA_CA_CERTS",
"DENO_CERT",
Expand Down Expand Up @@ -1461,7 +1453,6 @@ async fn run_lifecycle(
openshell_supervisor_network::host::HostProxyConfig {
bind_addr: addr,
policy: proxy_policy,
binary_path: host_proxy_binary_path(&sandbox_config),
client_auth: proxy_auth.host_client_auth(),
sandbox_id: Some(sandbox_id.clone()),
sandbox_name: Some(sandbox_name.clone()),
Expand Down
165 changes: 165 additions & 0 deletions crates/openshell-driver-mxc/tests/wxc_exec_real.rs
Original file line number Diff line number Diff line change
Expand Up @@ -961,6 +961,171 @@ async fn pc_https_egress_reads_injected_ca_bundle() {
);
}

/// Prove that host-proxy binary policy follows the process that owns each TCP
/// connection, rather than the sandbox entry command. This deliberately uses
/// L4 CONNECT policy so the assertion is independent of TLS/L7 enforcement.
#[tokio::test]
#[ignore = "requires real wxc-exec and outbound HTTPS"]
async fn pc_proxy_scopes_network_policy_to_socket_owner() {
let Some(wxc) = wxc_path() else {
eprintln!("SKIP: wxc-exec not found");
return;
};
if let Err(reason) = probe_processcontainer(&wxc) {
eprintln!("SKIP: processcontainer not live: {reason}");
return;
}

// QueryFullProcessImageNameW returns this Win32 spelling on the Windows
// test image. Keep the spelling exact here; path and case normalization
// are covered separately.
let cmd = PathBuf::from(r"C:\Windows\System32\cmd.exe");
let curl = PathBuf::from(r"C:\Windows\System32\curl.exe");
if !cmd.exists() || !curl.exists() {
eprintln!(
"SKIP: expected Windows binaries are absent (cmd={}, curl={})",
cmd.display(),
curl.display()
);
return;
}

let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
run_proxy_binary_scope_case(&wxc, "pc-owner-allow-child", &cmd, &curl, &curl, true).await;
run_proxy_binary_scope_case(&wxc, "pc-owner-deny-child", &cmd, &curl, &cmd, false).await;
}

async fn run_proxy_binary_scope_case(
wxc: &Path,
sandbox_id: &str,
cmd: &Path,
curl: &Path,
allowed_binary: &Path,
expect_allowed: bool,
) {
let output_dir = tempfile::tempdir().expect("proxy scope output directory");
let output_path = output_dir.path().join("example.html");
let diagnostic_path = output_dir.path().join("curl-diagnostic.txt");
let output_dir_string = output_dir.path().to_string_lossy().into_owned();
let command = vec![
cmd.to_string_lossy().into_owned(),
"/d".to_string(),
"/c".to_string(),
format!(
"echo proxy-scope 1>\"{}\" && \"{}\" --fail --silent --show-error --ssl-no-revoke --cacert \"%CURL_CA_BUNDLE%\" https://example.com/ --output \"{}\" 2>>\"{}\"",
diagnostic_path.display(),
curl.display(),
output_path.display(),
diagnostic_path.display()
),
];
let serde_json::Value::Object(driver_config) = serde_json::json!({
"command": command,
"cwd": output_dir_string,
}) else {
unreachable!();
};
let policy = SandboxPolicy {
version: 1,
filesystem: Some(FilesystemPolicy {
include_workdir: false,
read_only: Vec::new(),
read_write: vec![output_dir_string],
}),
network_policies: std::collections::HashMap::from([(
"https_example".to_string(),
NetworkPolicyRule {
name: "https-example".to_string(),
endpoints: vec![NetworkEndpoint {
host: "example.com".to_string(),
ports: vec![443],
..Default::default()
}],
binaries: vec![NetworkBinary {
path: allowed_binary.to_string_lossy().into_owned(),
}],
},
)]),
..Default::default()
};
let sandbox = DriverSandbox {
id: sandbox_id.to_string(),
name: sandbox_id.to_string(),
spec: Some(DriverSandboxSpec {
template: Some(DriverSandboxTemplate {
driver_config: Some(
openshell_core::proto_struct::json_object_to_struct(driver_config)
.expect("driver config"),
),
..Default::default()
}),
policy: Some(policy),
..Default::default()
}),
..Default::default()
};
let backend = MxcComputeBackend::new(MxcComputeConfig {
wxc_exec_path: wxc.to_string_lossy().into_owned(),
egress_proxy: true,
egress_proxy_addr: "127.0.0.1:18080".to_string(),
..Default::default()
});
backend
.create_sandbox(&sandbox)
.await
.expect("real proxy-scope sandbox create accepted");

let mut terminal_condition = None;
for _ in 0..600 {
if let Some(observed) = backend.get_sandbox(sandbox_id).await
&& let Some(condition) = observed
.status
.and_then(|status| status.conditions.into_iter().find(|c| c.r#type == "Ready"))
&& matches!(
condition.reason.as_str(),
"AgentCompleted" | "ExecFailed" | "ProvisionFailed"
)
{
terminal_condition = Some(condition);
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
let condition = terminal_condition.expect("proxy-scope sandbox should terminate");
let diagnostic = std::fs::read_to_string(&diagnostic_path)
.unwrap_or_else(|error| format!("failed to read curl diagnostic: {error}"));
backend
.delete_sandbox(sandbox_id, sandbox_id)
.await
.expect("delete completed proxy-scope sandbox");

if expect_allowed {
assert_eq!(
condition.reason, "AgentCompleted",
"declared child binary must be allowed: {}; diagnostic: {diagnostic}",
condition.message
);
assert!(
std::fs::metadata(&output_path).is_ok_and(|metadata| metadata.len() > 0),
"allowed curl response should be non-empty; diagnostic: {diagnostic}"
);
} else {
assert_eq!(
condition.reason, "ExecFailed",
"entry-command grant must not be inherited by curl: {}; diagnostic: {diagnostic}",
condition.message
);
assert!(
diagnostic.contains("403"),
"undeclared curl child should receive proxy 403; diagnostic: {diagnostic}"
);
assert!(
!output_path.exists(),
"denied curl child must not write an HTTPS response"
);
}
}

/// Write to a path OUTSIDE the granted dir; assert exit non-zero and file absent.
/// This is the genuine OS default-deny proof — the `AppContainer` blocks the write
/// without requiring any host ACL lockdown. The mock can only fake this.
Expand Down
3 changes: 3 additions & 0 deletions crates/openshell-supervisor-network/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ tokio-stream = { workspace = true, features = ["net"] }
[target.'cfg(unix)'.dependencies]
libc = "0.2"

[target.'cfg(target_os = "windows")'.dependencies]
windows = { workspace = true }

[target.'cfg(unix)'.dev-dependencies]

[lints]
Expand Down
Loading
Loading