From 507e4db17aea2d4f05cbc61b04822b984f29908b Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Sat, 19 Sep 2026 14:27:14 -0700 Subject: [PATCH 1/3] test(mxc): qualify HTTPS L7 enforcement (NVBug 6783374) Signed-off-by: Prekshi Vyas --- crates/openshell-driver-mxc/README.md | 17 ++-- .../tests/wxc_exec_real.rs | 83 ++++++------------- 2 files changed, 37 insertions(+), 63 deletions(-) diff --git a/crates/openshell-driver-mxc/README.md b/crates/openshell-driver-mxc/README.md index b8c52f9898..16f0ae0d46 100644 --- a/crates/openshell-driver-mxc/README.md +++ b/crates/openshell-driver-mxc/README.md @@ -156,11 +156,18 @@ process env. In curated-environment mode, the driver stages the public CA files under the authorized `/.openshell-proxy/` directory. Other environment modes grant the sandbox's unique public-CA directory as an internal read-write share. The 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. +the ephemeral CA private key remains in the host proxy's memory. + +Windows inbox `curl.exe` uses Schannel and ignores `CURL_CA_BUNDLE` as an +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. diff --git a/crates/openshell-driver-mxc/tests/wxc_exec_real.rs b/crates/openshell-driver-mxc/tests/wxc_exec_real.rs index 36b6edb804..1aacf902ed 100644 --- a/crates/openshell-driver-mxc/tests/wxc_exec_real.rs +++ b/crates/openshell-driver-mxc/tests/wxc_exec_real.rs @@ -557,58 +557,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 @@ -816,11 +764,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) { - 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"); @@ -832,10 +775,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 @@ -849,8 +796,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(), @@ -962,6 +916,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. From 4c6168a2df27332ade258fcfe3006f5b15ecf8c9 Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Mon, 21 Sep 2026 23:21:46 -0700 Subject: [PATCH 2/3] docs(mxc): clarify real qualification failures Signed-off-by: Prekshi Vyas --- .agents/skills/build-openshell-mxc-windows/SKILL.md | 9 ++++++--- docs/reference/gateway-config.mdx | 2 ++ docs/security/best-practices.mdx | 2 ++ tasks/windows.toml | 4 ++-- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.agents/skills/build-openshell-mxc-windows/SKILL.md b/.agents/skills/build-openshell-mxc-windows/SKILL.md index 1120e6cfcc..56a553e68a 100644 --- a/.agents/skills/build-openshell-mxc-windows/SKILL.md +++ b/.agents/skills/build-openshell-mxc-windows/SKILL.md @@ -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 full validation, detect the Windows host architecture first and choose the diff --git a/docs/reference/gateway-config.mdx b/docs/reference/gateway-config.mdx index 44de908678..53f5eeb882 100644 --- a/docs/reference/gateway-config.mdx +++ b/docs/reference/gateway-config.mdx @@ -951,6 +951,8 @@ etw_audit = false 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 `/.openshell-proxy//`. 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. diff --git a/docs/security/best-practices.mdx b/docs/security/best-practices.mdx index bd7cd5ff61..4fff61f276 100644 --- a/docs/security/best-practices.mdx +++ b/docs/security/best-practices.mdx @@ -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. diff --git a/tasks/windows.toml b/tasks/windows.toml index 7509a934c6..05bb87c1d1 100644 --- a/tasks/windows.toml +++ b/tasks/windows.toml @@ -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" From fce81292568943d038a01d0181a700fb892e9b40 Mon Sep 17 00:00:00 2001 From: Shailendra Singh Date: Tue, 22 Sep 2026 11:40:00 -0700 Subject: [PATCH 3/3] docs(mxc): align real test skip semantics Signed-off-by: Shailendra Singh --- crates/openshell-driver-mxc/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/openshell-driver-mxc/README.md b/crates/openshell-driver-mxc/README.md index 039cfcf4ea..a668c4bb01 100644 --- a/crates/openshell-driver-mxc/README.md +++ b/crates/openshell-driver-mxc/README.md @@ -201,9 +201,10 @@ This example uses `process_container`. The `IsoSessionApp.dll` and ## Real-MXC test lane -Three tasks drive real `wxc-exec.exe` hardware; all are **skip-safe** — any test -or scenario that requires an absent binary or backend prints a SKIP reason and -exits 0 rather than failing. +Three tasks drive real `wxc-exec.exe` hardware. They print a SKIP reason and +exit 0 when `wxc-exec` or the requested backend is unavailable. Once +ProcessContainer is live, required capabilities are authoritative: rejection +of `network.proxy` or another enforcement failure fails the task. | Task | What it runs | When to use | |---|---|---|