fix(sandbox): opt Node clients into proxy env support (#269)

This commit is contained in:
Piotr Mlocek
2026-03-12 17:43:07 -07:00
committed by GitHub
parent 14e296d318
commit b241237bde
6 changed files with 106 additions and 37 deletions
+1 -1
View File
@@ -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:
+2 -1
View File
@@ -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):
@@ -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) |
+77
View File
@@ -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"));
}
}
+3 -2
View File
@@ -5,6 +5,7 @@
//!
//! This crate provides process sandboxing and monitoring capabilities.
mod child_env;
pub mod denial_aggregator;
mod grpc_client;
mod identity;
@@ -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);
+16 -23
View File
@@ -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")]
@@ -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).
@@ -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).
+7 -10
View File
@@ -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;
@@ -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 {