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
2 changes: 1 addition & 1 deletion architecture/sandbox-connect.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ Authorization is performed by the gateway (token validation + sandbox readiness
2. Clones the master fd for reading and writing
3. Configures the shell command with environment variables:
- `OPENSHELL_SANDBOX=1`, `HOME=/sandbox`, `USER=sandbox`, `TERM=<from pty request>`
- Proxy vars: `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `http_proxy`, `https_proxy`, `grpc_proxy`
- Proxy vars: `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `http_proxy`, `https_proxy`, `grpc_proxy`, `NODE_USE_ENV_PROXY=1` so Node.js `fetch` honors the proxy env
- TLS trust vars: `NODE_EXTRA_CA_CERTS`, `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE`
- Provider credential env vars (from the provider registry)
4. Installs a `pre_exec` hook that:
Expand Down
3 changes: 2 additions & 1 deletion architecture/sandbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -929,7 +929,7 @@ Wraps `tokio::process::Child` + PID. Platform-specific `spawn()` methods delegat
**Environment setup** (both Linux and non-Linux):
- `OPENSHELL_SANDBOX=1` (always set)
- Provider credentials (from `GetSandboxProviderEnvironment` RPC)
- Proxy URLs: `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY` (uppercase for curl/wget), `http_proxy`, `https_proxy`, `grpc_proxy` (lowercase for gRPC C-core)
- Proxy URLs: `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY` (uppercase for curl/wget), `http_proxy`, `https_proxy`, `grpc_proxy` (lowercase for gRPC C-core), `NODE_USE_ENV_PROXY=1` (required for Node.js built-in `fetch`/`http` clients to honor proxy env vars)
- TLS trust store: `NODE_EXTRA_CA_CERTS` (standalone CA cert), `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE` (combined bundle)

**Pre-exec closure** (runs in child after fork, before exec -- async-signal-safe):
Expand Down Expand Up @@ -1057,6 +1057,7 @@ This two-phase approach (peek with `WNOWAIT`, then selectively reap) avoids `ECH
| `OPENSHELL_SANDBOX` | Always `"1"` -- signals the process is sandboxed |
| `HTTP_PROXY` / `HTTPS_PROXY` / `ALL_PROXY` | Proxy URL (uppercase, for curl/wget) |
| `http_proxy` / `https_proxy` / `grpc_proxy` | Proxy URL (lowercase, for gRPC C-core) |
| `NODE_USE_ENV_PROXY` | Set to `1` so Node.js built-in `fetch`/`http` clients honor proxy env vars |
| `NODE_EXTRA_CA_CERTS` | Path to sandbox CA cert PEM (Node.js, additive) |
| `SSL_CERT_FILE` | Combined CA bundle path (OpenSSL/Python/Go) |
| `REQUESTS_CA_BUNDLE` | Combined CA bundle path (Python requests) |
Expand Down
77 changes: 77 additions & 0 deletions crates/navigator-sandbox/src/child_env.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

use std::path::Path;

pub(crate) fn proxy_env_vars(proxy_url: &str) -> [(&'static str, String); 7] {
[
("ALL_PROXY", proxy_url.to_owned()),
("HTTP_PROXY", proxy_url.to_owned()),
("HTTPS_PROXY", proxy_url.to_owned()),
("http_proxy", proxy_url.to_owned()),
("https_proxy", proxy_url.to_owned()),
("grpc_proxy", proxy_url.to_owned()),
// Node.js only honors HTTP(S)_PROXY for built-in fetch/http clients when
// proxy support is explicitly enabled at process startup.
("NODE_USE_ENV_PROXY", "1".to_owned()),
]
}

pub(crate) fn tls_env_vars(
ca_cert_path: &Path,
combined_bundle_path: &Path,
) -> [(&'static str, String); 4] {
let ca_cert_path = ca_cert_path.display().to_string();
let combined_bundle_path = combined_bundle_path.display().to_string();
[
("NODE_EXTRA_CA_CERTS", ca_cert_path.clone()),
("SSL_CERT_FILE", combined_bundle_path.clone()),
("REQUESTS_CA_BUNDLE", combined_bundle_path.clone()),
("CURL_CA_BUNDLE", combined_bundle_path),
]
}

#[cfg(test)]
mod tests {
use super::*;
use std::process::Command;
use std::process::Stdio;

#[test]
fn apply_proxy_env_includes_node_proxy_opt_in() {
let mut cmd = Command::new("/usr/bin/env");
cmd.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null());

for (key, value) in proxy_env_vars("http://10.200.0.1:3128") {
cmd.env(key, value);
}

let output = cmd.output().expect("spawn env");
let stdout = String::from_utf8(output.stdout).expect("utf8");

assert!(stdout.contains("HTTP_PROXY=http://10.200.0.1:3128"));
assert!(stdout.contains("NODE_USE_ENV_PROXY=1"));
}

#[test]
fn apply_tls_env_sets_node_and_bundle_paths() {
let mut cmd = Command::new("/usr/bin/env");
cmd.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null());

let ca_cert_path = Path::new("/etc/navigator-tls/navigator-ca.pem");
let combined_bundle_path = Path::new("/etc/navigator-tls/ca-bundle.pem");
for (key, value) in tls_env_vars(ca_cert_path, combined_bundle_path) {
cmd.env(key, value);
}

let output = cmd.output().expect("spawn env");
let stdout = String::from_utf8(output.stdout).expect("utf8");

assert!(stdout.contains("NODE_EXTRA_CA_CERTS=/etc/navigator-tls/navigator-ca.pem"));
assert!(stdout.contains("SSL_CERT_FILE=/etc/navigator-tls/ca-bundle.pem"));
}
}
5 changes: 3 additions & 2 deletions crates/navigator-sandbox/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//!
//! This crate provides process sandboxing and monitoring capabilities.

mod child_env;
pub mod denial_aggregator;
mod grpc_client;
mod identity;
Expand Down Expand Up @@ -339,8 +340,8 @@ pub async fn run_sandbox(
// SSH shell processes need both to enforce network policy:
// - netns_fd: enter the network namespace via setns() so all traffic
// goes through the veth pair (hard enforcement, non-bypassable)
// - proxy_url: set HTTP_PROXY/HTTPS_PROXY/ALL_PROXY env vars so
// cooperative tools (curl, etc.) route through the CONNECT proxy
// - proxy_url: set proxy env vars so cooperative tools route through the
// CONNECT proxy; this also opts Node.js into honoring those vars
#[cfg(target_os = "linux")]
let ssh_netns_fd = netns.as_ref().and_then(NetworkNamespace::ns_fd);

Expand Down
39 changes: 16 additions & 23 deletions crates/navigator-sandbox/src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

//! Process management and signal handling.

use crate::child_env;
use crate::policy::{NetworkMode, SandboxPolicy};
use crate::sandbox;
#[cfg(target_os = "linux")]
Expand Down Expand Up @@ -135,29 +136,22 @@ impl ProcessHandle {
let proxy_url = format!("http://10.200.0.1:{port}");
// Both uppercase and lowercase variants: curl/wget use uppercase,
// gRPC C-core (libgrpc) checks lowercase http_proxy/https_proxy.
cmd.env("ALL_PROXY", &proxy_url)
.env("HTTP_PROXY", &proxy_url)
.env("HTTPS_PROXY", &proxy_url)
.env("http_proxy", &proxy_url)
.env("https_proxy", &proxy_url)
.env("grpc_proxy", &proxy_url);
for (key, value) in child_env::proxy_env_vars(&proxy_url) {
cmd.env(key, value);
}
} else if let Some(http_addr) = proxy.http_addr {
let proxy_url = format!("http://{http_addr}");
cmd.env("ALL_PROXY", &proxy_url)
.env("HTTP_PROXY", &proxy_url)
.env("HTTPS_PROXY", &proxy_url)
.env("http_proxy", &proxy_url)
.env("https_proxy", &proxy_url)
.env("grpc_proxy", &proxy_url);
for (key, value) in child_env::proxy_env_vars(&proxy_url) {
cmd.env(key, value);
}
}
}

// Set TLS trust store env vars so sandbox processes trust the ephemeral CA
if let Some((ca_cert_path, combined_bundle_path)) = ca_paths {
cmd.env("NODE_EXTRA_CA_CERTS", ca_cert_path) // Node.js (additive)
.env("SSL_CERT_FILE", combined_bundle_path) // OpenSSL/Python/Go
.env("REQUESTS_CA_BUNDLE", combined_bundle_path) // Python requests
.env("CURL_CA_BUNDLE", combined_bundle_path); // curl/libcurl
for (key, value) in child_env::tls_env_vars(ca_cert_path, combined_bundle_path) {
cmd.env(key, value);
}
}

// Set up process group for signal handling (non-interactive mode only).
Expand Down Expand Up @@ -240,18 +234,17 @@ impl ProcessHandle {
})?;
if let Some(http_addr) = proxy.http_addr {
let proxy_url = format!("http://{http_addr}");
cmd.env("ALL_PROXY", &proxy_url)
.env("HTTP_PROXY", &proxy_url)
.env("HTTPS_PROXY", &proxy_url);
for (key, value) in child_env::proxy_env_vars(&proxy_url) {
cmd.env(key, value);
}
}
}

// Set TLS trust store env vars so sandbox processes trust the ephemeral CA
if let Some((ca_cert_path, combined_bundle_path)) = ca_paths {
cmd.env("NODE_EXTRA_CA_CERTS", ca_cert_path)
.env("SSL_CERT_FILE", combined_bundle_path)
.env("REQUESTS_CA_BUNDLE", combined_bundle_path)
.env("CURL_CA_BUNDLE", combined_bundle_path);
for (key, value) in child_env::tls_env_vars(ca_cert_path, combined_bundle_path) {
cmd.env(key, value);
}
}

// Set up process group for signal handling (non-interactive mode only).
Expand Down
17 changes: 7 additions & 10 deletions crates/navigator-sandbox/src/ssh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

//! Embedded SSH server for sandbox access.

use crate::child_env;
use crate::policy::SandboxPolicy;
use crate::process::drop_privileges;
use crate::sandbox;
Expand Down Expand Up @@ -668,19 +669,15 @@ fn apply_child_env(
.env("TERM", term);

if let Some(url) = proxy_url {
cmd.env("HTTP_PROXY", url)
.env("HTTPS_PROXY", url)
.env("ALL_PROXY", url)
.env("http_proxy", url)
.env("https_proxy", url)
.env("grpc_proxy", url);
for (key, value) in child_env::proxy_env_vars(url) {
cmd.env(key, value);
}
}

if let Some((ca_cert_path, combined_bundle_path)) = ca_file_paths {
cmd.env("NODE_EXTRA_CA_CERTS", ca_cert_path)
.env("SSL_CERT_FILE", combined_bundle_path)
.env("REQUESTS_CA_BUNDLE", combined_bundle_path)
.env("CURL_CA_BUNDLE", combined_bundle_path);
for (key, value) in child_env::tls_env_vars(ca_cert_path, combined_bundle_path) {
cmd.env(key, value);
}
}

for (key, value) in provider_env {
Expand Down
Loading