Skip to content
Merged
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
9 changes: 6 additions & 3 deletions .agents/skills/build-openshell-mxc-windows/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,12 @@ exclusive on a single host (each rejects the other architecture -- see the
table below): run `windows:test:mxc-real:x64` on an x64 host, or
`windows:test:mxc-real:arm64` on an ARM64 host, as part of validating this
subsystem -- run the one matching your host architecture, not both, and not
neither. Both are skip-safe (they print a SKIP reason and exit 0 when
`wxc-exec` or the matching backend isn't available), so running the
arch-appropriate task is always safe even without real MXC hardware. Neither
neither. Both print a SKIP reason and exit 0 when `wxc-exec` or the matching
backend is unavailable. Once the host proves that ProcessContainer is live,
however, required capabilities are authoritative: rejection of
`network.proxy` or another enforcement failure fails the task. Running the
architecture-appropriate task is therefore safe without real MXC hardware but
must not be described as wholly skip-safe on supported hardware. Neither task
is part of `windows:ci`'s ordered contract, so invoke it explicitly.

For GB300 Windows ARM64 qualification, do not use the skip-safe developer task
Expand Down
27 changes: 18 additions & 9 deletions crates/openshell-driver-mxc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,18 @@ process env. In every environment mode, the driver stages the public CA files
under the authorized `<cwd>/.openshell-proxy/<sandbox-id>` directory. The host
proxy's private temporary directory is never added to the sandbox's read-write
shares. The staged directory contains only public CA certificates; the ephemeral
CA private key remains in the host proxy's memory. The driver
seeds only `SYSTEMROOT`, `WINDIR`, `PATH`, `COMSPEC`, and `LOCALAPPDATA` from the
gateway host before applying sandbox and TLS overrides, so required Windows
bootstrap values remain available without exposing the gateway's full
environment unless the gateway explicitly opts into another environment mode.
CA private key remains in the host proxy's memory.

Windows inbox `curl.exe` uses Schannel and ignores `CURL_CA_BUNDLE` as an
Comment thread
prekshivyas marked this conversation as resolved.
environment variable, so workloads using it must pass
`--cacert %CURL_CA_BUNDLE%` explicitly. Clients that honor the injected trust
variables consume the same per-sandbox bundle directly.

The driver seeds only `SYSTEMROOT`, `WINDIR`, `PATH`, `COMSPEC`, and
`LOCALAPPDATA` from the gateway host before applying sandbox and TLS overrides,
so required Windows bootstrap values remain available without exposing the
gateway's full environment unless the gateway explicitly opts into another
environment mode.

When governed egress is disabled, any network rule fails closed during sandbox creation.

Expand All @@ -194,10 +201,12 @@ This example uses `process_container`. The `IsoSessionApp.dll` and

## Real-MXC test lane

The generic real-`wxc-exec.exe` tasks are **skip-safe**: a test or scenario that
requires an absent binary or backend prints a SKIP reason and exits 0. They are
useful developer diagnostics, but a skipped run is not qualification evidence.
The GB300 task is deliberately strict and fails on every required skip.
The generic real-`wxc-exec.exe` tasks print a SKIP reason and exit 0 when the
binary or requested backend is unavailable. Once ProcessContainer is live,
required capabilities are authoritative: rejection of `network.proxy` or
another enforcement failure fails the task. These tasks are useful developer
diagnostics, but a skipped run is not qualification evidence. The GB300 task
is deliberately strict and fails on every required skip.

| Task | What it runs | When to use |
|---|---|---|
Expand Down
83 changes: 25 additions & 58 deletions crates/openshell-driver-mxc/tests/wxc_exec_real.rs
Original file line number Diff line number Diff line change
Expand Up @@ -659,58 +659,6 @@ fn probe_processcontainer(wxc: &PathBuf) -> Result<(), String> {
Ok(())
}

/// Probe the released binary's live `network.proxy` support separately from
/// ordinary `ProcessContainer` support. Some builds accept the proxy JSON during
/// `--dry-run` but return `ERROR_INVALID_PARAMETER` from the live launcher.
fn probe_processcontainer_proxy(wxc: &PathBuf) -> Result<(), String> {
let (_tempdir, temp_path) = temp_fixture();
let proxy_listener = std::net::TcpListener::bind("127.0.0.1:0")
.map_err(|error| format!("failed to reserve proxy probe port: {error}"))?;
let proxy_port = proxy_listener
.local_addr()
.map_err(|error| format!("failed to read proxy probe port: {error}"))?
.port();
let config = serde_json::json!({
"version": "0.6.0-alpha",
"containerId": "probe-pc-proxy",
"containment": "processcontainer",
"process": {
"commandLine": "C:\\Windows\\System32\\cmd.exe /c exit 0",
"cwd": temp_path,
"timeout": 30_000,
},
"filesystem": {
"readwritePaths": [temp_path],
},
"processContainer": {
"leastPrivilege": false,
},
"network": {
"defaultPolicy": "block",
"proxy": { "localhost": proxy_port },
},
});

let json = serde_json::to_string(&config).unwrap();
let b64 = base64::engine::general_purpose::STANDARD.encode(json.as_bytes());
let output = Command::new(wxc)
.arg("--config-base64")
.arg(&b64)
.output()
.map_err(|error| format!("wxc-exec proxy probe failed to spawn: {error}"))?;

if output.status.success() {
return Ok(());
}

Err(format!(
"live network.proxy probe returned exit {}: stdout={} stderr={}",
output.status.code().unwrap_or(-1),
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
))
}

/// Probe the `isolation_session` backend.
///
/// Attempts a `provision` phase. Returns `Ok(sandbox_id)` when live, or
Expand Down Expand Up @@ -1013,11 +961,6 @@ async fn pc_https_egress_reads_injected_ca_bundle() {
eprintln!("SKIP: processcontainer not live: {reason}");
return;
}
if let Err(reason) = probe_processcontainer_proxy(&wxc) {
Comment thread
prekshivyas marked this conversation as resolved.
eprintln!("SKIP: processcontainer network.proxy not live: {reason}");
return;
}

let system_root = std::env::var("SYSTEMROOT").expect("SYSTEMROOT must be set on Windows");
let cmd = PathBuf::from(&system_root).join("System32").join("cmd.exe");
let curl = PathBuf::from(system_root).join("System32").join("curl.exe");
Expand All @@ -1029,10 +972,14 @@ async fn pc_https_egress_reads_injected_ca_bundle() {
let output_dir = tempfile::tempdir().expect("HTTPS output directory");
let output_path = output_dir.path().join("example.html");
let certificate_path = output_dir.path().join("peer-certificate.txt");
let post_response_path = output_dir.path().join("post-response.json");
let post_status_path = output_dir.path().join("post-status.txt");
let diagnostic_path = output_dir.path().join("https-diagnostic.txt");
let output_dir_string = output_dir.path().to_string_lossy().into_owned();
let output_path_string = output_path.to_string_lossy().into_owned();
let certificate_path_string = certificate_path.to_string_lossy().into_owned();
let post_response_path_string = post_response_path.to_string_lossy().into_owned();
let post_status_path_string = post_status_path.to_string_lossy().into_owned();
let diagnostic_path_string = diagnostic_path.to_string_lossy().into_owned();
let cmd_string = cmd.to_string_lossy().into_owned();
// Schannel's revocation lookup targets are intentionally outside this
Expand All @@ -1046,8 +993,15 @@ async fn pc_https_egress_reads_injected_ca_bundle() {
--cacert \"%CURL_CA_BUNDLE%\" \
https://example.com/ --output \"{output_path_string}\" \
--write-out \"%{{certs}}\" 1>\"{certificate_path_string}\" \
2>>\"{diagnostic_path_string}\" && \
\"{}\" --silent --show-error --ssl-no-revoke \
--cacert \"%CURL_CA_BUNDLE%\" --request POST \
--header \"Content-Type: application/json\" --data \"{{}}\" \
https://example.com/ --output \"{post_response_path_string}\" \
--write-out \"%{{http_code}}\" 1>\"{post_status_path_string}\" \
2>>\"{diagnostic_path_string}\"",
curl.display()
curl.display(),
curl.display(),
);
let command = vec![
cmd_string.clone(),
Expand Down Expand Up @@ -1156,6 +1110,19 @@ async fn pc_https_egress_reads_injected_ca_bundle() {
peer_certificate.contains("OpenShell Sandbox CA"),
"HTTPS response must use a certificate issued by the host proxy CA"
);
let post_status = std::fs::read_to_string(post_status_path).expect("POST status output");
assert_eq!(
post_status.trim(),
"403",
"read-only policy must deny HTTPS POST"
);
let post_response =
std::fs::read_to_string(post_response_path).expect("POST denial response body");
assert!(
post_response.contains("policy_denied")
|| post_response.contains("no matching L7 allow rule"),
"POST denial must come from the OpenShell L7 policy: {post_response}"
);
}

/// Write to a path OUTSIDE the granted dir; assert exit non-zero and file absent.
Expand Down
2 changes: 2 additions & 0 deletions docs/reference/gateway-config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,8 @@ non-mutating administrator-gated Service Control Manager open.

Set `egress_proxy = true` with `egress_proxy_addr = "127.0.0.1:18080"` to enable the Windows Pattern C split. The address must be a `127.0.0.1:PORT` socket. The driver allocates a unique ephemeral port per sandbox, injects that listener through proxy environment variables, and stages the public proxy CA beneath the sandbox's configured `<cwd>/.openshell-proxy/<sandbox-id>/`. A non-empty per-sandbox `cwd` is therefore required when governed egress is enabled; sandbox-specific subdirectories prevent concurrent sandboxes from overwriting each other's trust files. MXC denies direct Internet egress but allows `127.0.0.1/32`; this permits dynamic forwarding but also means the sandbox can reach unrelated host services bound to loopback.

The driver points common TLS trust variables, including `CURL_CA_BUNDLE`, at the staged public CA bundle. Windows inbox `curl.exe` uses Schannel and does not honor `CURL_CA_BUNDLE` automatically. Invoke it with `--cacert %CURL_CA_BUNDLE%` so HTTPS requests through the governed-egress proxy trust the per-sandbox CA.

MXC rejects policies containing `network_middlewares` before launch because this host-proxy path does not receive the gateway middleware registry.

Supply the workload command and optional working directory through `sandbox create --driver-config-json`, for example `{"mxc":{"command":["cmd","/c","echo hello"],"cwd":"C:\\work"}}`. Supply workload environment through `sandbox create --env` or `--env-from`; it is not part of gateway configuration.
Expand Down
2 changes: 2 additions & 0 deletions docs/security/best-practices.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ This enables credential injection and L7 inspection without explicit configurati
| Risk if relaxed | `tls: skip` disables placeholder credential rewriting, dynamic token grant injection, and L7 inspection for that endpoint. The proxy relays encrypted traffic without seeing the contents. |
| Recommendation | Use auto-detect (the default) for most endpoints. Use `tls: skip` only when the upstream requires the client's own TLS certificate (mTLS) or uses a non-HTTP protocol. A provider-credentialed endpoint also requires the explicit `allow_uninspected_credentials: true` exception. |

On Windows, inbox `curl.exe` uses Schannel and does not automatically honor the injected `CURL_CA_BUNDLE` value. Pass `--cacert %CURL_CA_BUNDLE%` explicitly when that client sends HTTPS traffic through an MXC governed-egress proxy. Other clients continue to use the trust variable they support.

### SSRF Protection

After OPA policy allows a connection, the proxy resolves DNS and rejects undeclared internal destinations.
Expand Down
4 changes: 2 additions & 2 deletions tasks/windows.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,12 @@ run = "echo 'windows:* tasks require a Windows MSVC host' && exit 1"
run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-msvc.ps1 ci all"

["windows:test:mxc-real:x64"]
description = "Run native x64 real-wxc-exec Tier-2 integration tests (skip-safe: tests print SKIP when binary/backend absent)"
description = "Run native x64 real-wxc-exec Tier-2 integration tests (skip only when binary/backend is unavailable; supported ProcessContainer failures are fatal)"
run = "echo 'windows:* tasks require a Windows MSVC host' && exit 1"
run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-msvc.ps1 test-mxc-real x86_64-pc-windows-msvc"

["windows:test:mxc-real:arm64"]
description = "Run native ARM64 real-wxc-exec Tier-2 integration tests (skip-safe: tests print SKIP when binary/backend absent)"
description = "Run native ARM64 real-wxc-exec Tier-2 integration tests (skip only when binary/backend is unavailable; supported ProcessContainer failures are fatal)"
run = "echo 'windows:* tasks require a Windows MSVC host' && exit 1"
run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-msvc.ps1 test-mxc-real aarch64-pc-windows-msvc"

Expand Down
Loading