Skip to content

Commit 507e4db

Browse files
committed
test(mxc): qualify HTTPS L7 enforcement (NVBug 6783374)
Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
1 parent fb2980e commit 507e4db

2 files changed

Lines changed: 37 additions & 63 deletions

File tree

crates/openshell-driver-mxc/README.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,11 +156,18 @@ process env. In curated-environment mode, the driver stages the public CA files
156156
under the authorized `<cwd>/.openshell-proxy/<sandbox-id>` directory. Other
157157
environment modes grant the sandbox's unique public-CA directory as an internal
158158
read-write share. The directory contains only public CA certificates;
159-
the ephemeral CA private key remains in the host proxy's memory. The driver
160-
seeds only `SYSTEMROOT`, `WINDIR`, `PATH`, `COMSPEC`, and `LOCALAPPDATA` from the
161-
gateway host before applying sandbox and TLS overrides, so required Windows
162-
bootstrap values remain available without exposing the gateway's full
163-
environment unless the gateway explicitly opts into another environment mode.
159+
the ephemeral CA private key remains in the host proxy's memory.
160+
161+
Windows inbox `curl.exe` uses Schannel and ignores `CURL_CA_BUNDLE` as an
162+
environment variable, so workloads using it must pass
163+
`--cacert %CURL_CA_BUNDLE%` explicitly. Clients that honor the injected trust
164+
variables consume the same per-sandbox bundle directly.
165+
166+
The driver seeds only `SYSTEMROOT`, `WINDIR`, `PATH`, `COMSPEC`, and
167+
`LOCALAPPDATA` from the gateway host before applying sandbox and TLS overrides,
168+
so required Windows bootstrap values remain available without exposing the
169+
gateway's full environment unless the gateway explicitly opts into another
170+
environment mode.
164171

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

crates/openshell-driver-mxc/tests/wxc_exec_real.rs

Lines changed: 25 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -557,58 +557,6 @@ fn probe_processcontainer(wxc: &PathBuf) -> Result<(), String> {
557557
Ok(())
558558
}
559559

560-
/// Probe the released binary's live `network.proxy` support separately from
561-
/// ordinary `ProcessContainer` support. Some builds accept the proxy JSON during
562-
/// `--dry-run` but return `ERROR_INVALID_PARAMETER` from the live launcher.
563-
fn probe_processcontainer_proxy(wxc: &PathBuf) -> Result<(), String> {
564-
let (_tempdir, temp_path) = temp_fixture();
565-
let proxy_listener = std::net::TcpListener::bind("127.0.0.1:0")
566-
.map_err(|error| format!("failed to reserve proxy probe port: {error}"))?;
567-
let proxy_port = proxy_listener
568-
.local_addr()
569-
.map_err(|error| format!("failed to read proxy probe port: {error}"))?
570-
.port();
571-
let config = serde_json::json!({
572-
"version": "0.6.0-alpha",
573-
"containerId": "probe-pc-proxy",
574-
"containment": "processcontainer",
575-
"process": {
576-
"commandLine": "C:\\Windows\\System32\\cmd.exe /c exit 0",
577-
"cwd": temp_path,
578-
"timeout": 30_000,
579-
},
580-
"filesystem": {
581-
"readwritePaths": [temp_path],
582-
},
583-
"processContainer": {
584-
"leastPrivilege": false,
585-
},
586-
"network": {
587-
"defaultPolicy": "block",
588-
"proxy": { "localhost": proxy_port },
589-
},
590-
});
591-
592-
let json = serde_json::to_string(&config).unwrap();
593-
let b64 = base64::engine::general_purpose::STANDARD.encode(json.as_bytes());
594-
let output = Command::new(wxc)
595-
.arg("--config-base64")
596-
.arg(&b64)
597-
.output()
598-
.map_err(|error| format!("wxc-exec proxy probe failed to spawn: {error}"))?;
599-
600-
if output.status.success() {
601-
return Ok(());
602-
}
603-
604-
Err(format!(
605-
"live network.proxy probe returned exit {}: stdout={} stderr={}",
606-
output.status.code().unwrap_or(-1),
607-
String::from_utf8_lossy(&output.stdout),
608-
String::from_utf8_lossy(&output.stderr),
609-
))
610-
}
611-
612560
/// Probe the `isolation_session` backend.
613561
///
614562
/// Attempts a `provision` phase. Returns `Ok(sandbox_id)` when live, or
@@ -816,11 +764,6 @@ async fn pc_https_egress_reads_injected_ca_bundle() {
816764
eprintln!("SKIP: processcontainer not live: {reason}");
817765
return;
818766
}
819-
if let Err(reason) = probe_processcontainer_proxy(&wxc) {
820-
eprintln!("SKIP: processcontainer network.proxy not live: {reason}");
821-
return;
822-
}
823-
824767
let system_root = std::env::var("SYSTEMROOT").expect("SYSTEMROOT must be set on Windows");
825768
let cmd = PathBuf::from(&system_root).join("System32").join("cmd.exe");
826769
let curl = PathBuf::from(system_root).join("System32").join("curl.exe");
@@ -832,10 +775,14 @@ async fn pc_https_egress_reads_injected_ca_bundle() {
832775
let output_dir = tempfile::tempdir().expect("HTTPS output directory");
833776
let output_path = output_dir.path().join("example.html");
834777
let certificate_path = output_dir.path().join("peer-certificate.txt");
778+
let post_response_path = output_dir.path().join("post-response.json");
779+
let post_status_path = output_dir.path().join("post-status.txt");
835780
let diagnostic_path = output_dir.path().join("https-diagnostic.txt");
836781
let output_dir_string = output_dir.path().to_string_lossy().into_owned();
837782
let output_path_string = output_path.to_string_lossy().into_owned();
838783
let certificate_path_string = certificate_path.to_string_lossy().into_owned();
784+
let post_response_path_string = post_response_path.to_string_lossy().into_owned();
785+
let post_status_path_string = post_status_path.to_string_lossy().into_owned();
839786
let diagnostic_path_string = diagnostic_path.to_string_lossy().into_owned();
840787
let cmd_string = cmd.to_string_lossy().into_owned();
841788
// Schannel's revocation lookup targets are intentionally outside this
@@ -849,8 +796,15 @@ async fn pc_https_egress_reads_injected_ca_bundle() {
849796
--cacert \"%CURL_CA_BUNDLE%\" \
850797
https://example.com/ --output \"{output_path_string}\" \
851798
--write-out \"%{{certs}}\" 1>\"{certificate_path_string}\" \
799+
2>>\"{diagnostic_path_string}\" && \
800+
\"{}\" --silent --show-error --ssl-no-revoke \
801+
--cacert \"%CURL_CA_BUNDLE%\" --request POST \
802+
--header \"Content-Type: application/json\" --data \"{{}}\" \
803+
https://example.com/ --output \"{post_response_path_string}\" \
804+
--write-out \"%{{http_code}}\" 1>\"{post_status_path_string}\" \
852805
2>>\"{diagnostic_path_string}\"",
853-
curl.display()
806+
curl.display(),
807+
curl.display(),
854808
);
855809
let command = vec![
856810
cmd_string.clone(),
@@ -962,6 +916,19 @@ async fn pc_https_egress_reads_injected_ca_bundle() {
962916
peer_certificate.contains("OpenShell Sandbox CA"),
963917
"HTTPS response must use a certificate issued by the host proxy CA"
964918
);
919+
let post_status = std::fs::read_to_string(post_status_path).expect("POST status output");
920+
assert_eq!(
921+
post_status.trim(),
922+
"403",
923+
"read-only policy must deny HTTPS POST"
924+
);
925+
let post_response =
926+
std::fs::read_to_string(post_response_path).expect("POST denial response body");
927+
assert!(
928+
post_response.contains("policy_denied")
929+
|| post_response.contains("no matching L7 allow rule"),
930+
"POST denial must come from the OpenShell L7 policy: {post_response}"
931+
);
965932
}
966933

967934
/// Write to a path OUTSIDE the granted dir; assert exit non-zero and file absent.

0 commit comments

Comments
 (0)