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
4 changes: 4 additions & 0 deletions crates/openshell-driver-mxc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ etw_audit = false
Only `process_container` supports this architecture. `isolation_session` is
rejected during sandbox validation.

The host supervisor writes its proxy CA certificate and trust bundle to
`proxy-tls\` in the sandbox's host state directory,
`<state_dir>\<sandbox-id>\<generation>`, and deleting the sandbox removes them.

MXC 1.0.0 is required. Pre-1.0 executors and the retired
`default_configuration_id` setting are not supported; remove that setting
before upgrading. Both live launches and standalone mapper output use the
Expand Down
69 changes: 66 additions & 3 deletions crates/openshell-driver-mxc/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ const DRIVER_NAME: &str = "mxc";
const DRIVER_VERSION: &str = env!("CARGO_PKG_VERSION");
const DEFAULT_IMAGE_SENTINEL: &str = "mxc:process-container";
const HOST_AUTH_BUNDLE_FILE: &str = "supervisor-auth.json";
/// Directory in the sandbox host state directory for the proxy CA files.
const HOST_PROXY_TLS_DIR: &str = "proxy-tls";
const HOST_RUNTIME_DESCRIPTOR_FILE: &str = "runtime-descriptor.json";
const BOUNDARY_CONFIG_FILE: &str = "boundary.json";
const BOUNDARY_TLS_CERT_FILE: &str = "sandbox.crt";
Expand Down Expand Up @@ -858,6 +860,17 @@ fn spawn_supervisor(
auth_bundle_path: &Path,
main: &openshell_core::sandbox_env::MainProcessConfig,
) -> Result<Child, String> {
supervisor_command(context, descriptor_path, auth_bundle_path, main)?
.spawn()
.map_err(|error| format!("start host openshell-supervisor: {error}"))
}

fn supervisor_command(
context: &LifecycleContext,
descriptor_path: &Path,
auth_bundle_path: &Path,
main: &openshell_core::sandbox_env::MainProcessConfig,
) -> Result<Command, String> {
let main_process_spec = mxc_main_process_spec(main)?;
let mut command = Command::new(&context.config.supervisor_binary_path);
command
Expand Down Expand Up @@ -887,6 +900,14 @@ fn spawn_supervisor(
)
.env(openshell_core::sandbox_env::SANDBOX_ID, &context.sandbox.id)
.env(openshell_core::sandbox_env::SANDBOX, &context.sandbox.name)
// The supervisor's default `/etc/openshell-tls` resolves to
// `\etc\openshell-tls` on the current drive, where every sandbox on
// the host would write and read the same CA files. Keep them with the
// sandbox's host state, which `delete_sandbox` removes.
.env(
openshell_core::sandbox_env::PROXY_TLS_DIR,
context.host_state_dir.join(HOST_PROXY_TLS_DIR),
)
.env(
openshell_core::sandbox_env::LOG_LEVEL,
openshell_core::driver_utils::sandbox_log_level(&context.sandbox, "info"),
Expand All @@ -900,9 +921,7 @@ fn spawn_supervisor(
server_name,
);
}
command
.spawn()
.map_err(|error| format!("start host openshell-supervisor: {error}"))
Ok(command)
}

fn mxc_main_process_spec(
Expand Down Expand Up @@ -1848,6 +1867,50 @@ mod tests {
assert!(!backend.config.state_dir.exists());
}

#[test]
fn host_supervisor_proxy_tls_dir_is_in_sandbox_host_state() {
let (backend, sandbox, dir) = preflight_fixture();
let host_state_dir = backend
.config
.state_dir
.join(&sandbox.id)
.join("generation");
let context = LifecycleContext {
invoker: backend.invoker.clone(),
config: backend.config.clone(),
gateway: backend.gateway.clone(),
registry: backend.registry.clone(),
watch_tx: backend.watch_tx.clone(),
attribution: backend.attribution.clone(),
sandbox_config: sandbox_config(&sandbox).unwrap(),
sandbox,
generation: "generation".into(),
host_state_dir: host_state_dir.clone(),
boundary_state_dir: dir.path().join("boundary"),
policy_mapper: backend.policy_mapper.clone(),
};
let main = mxc_main_process_config(
context.sandbox.spec.as_ref(),
&context.sandbox_config.command,
);
let command = supervisor_command(
&context,
&host_state_dir.join(HOST_RUNTIME_DESCRIPTOR_FILE),
&host_state_dir.join(HOST_AUTH_BUNDLE_FILE),
&main,
)
.unwrap();
let tls_dir = command
.as_std()
.get_envs()
.find(|(key, _)| *key == openshell_core::sandbox_env::PROXY_TLS_DIR)
.and_then(|(_, value)| value);
assert_eq!(
tls_dir.map(Path::new),
Some(host_state_dir.join(HOST_PROXY_TLS_DIR).as_path())
);
}

fn with_ui_policy(
mut sandbox: DriverSandbox,
ui: openshell_core::proto::UiPolicy,
Expand Down
63 changes: 63 additions & 0 deletions crates/openshell-supervisor-network/src/l7/tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,15 @@ fn write_tls_output(path: &Path, contents: &[u8]) -> Result<()> {
Err(error) => return Err(error).into_diagnostic(),
}

// `NamedTempFile::persist` passes the temporary file's path to
// `SetFileAttributesW` and both paths to `MoveFileExW` unchanged, and those
// calls fail for paths longer than `MAX_PATH` unless they carry the `\\?\`
// prefix. A sandbox's host state directory can be that deep.
#[cfg(windows)]
let extended = extended_length_path(path);
#[cfg(windows)]
let path = extended.as_path();

let parent = path
.parent()
.ok_or_else(|| miette!("TLS output has no parent: {}", path.display()))?;
Expand All @@ -418,6 +427,19 @@ fn write_tls_output(path: &Path, contents: &[u8]) -> Result<()> {
Ok(())
}

/// `path` with its directory in the extended-length (`\\?\`) form that
/// `canonicalize` returns on Windows, or `path` unchanged if `canonicalize`
/// fails. It can fail on volumes that the Mount Manager does not track, where
/// paths shorter than `MAX_PATH` still work as given.
#[cfg(windows)]
fn extended_length_path(path: &Path) -> PathBuf {
let (Some(parent), Some(file_name)) = (path.parent(), path.file_name()) else {
return path.to_path_buf();
};
std::fs::canonicalize(parent)
.map_or_else(|_| path.to_path_buf(), |parent| parent.join(file_name))
}

/// Load PEM-encoded certificates from a string into a root certificate store.
///
/// Returns `(added, ignored)` counts. Invalid or unparseable certificates
Expand Down Expand Up @@ -882,6 +904,47 @@ mod tests {
}
}

#[cfg(windows)]
#[test]
fn write_ca_files_accepts_paths_longer_than_max_path() {
use std::os::windows::ffi::OsStrExt;

let ca = SandboxCa::generate().unwrap();
let dir = tempfile::tempdir().unwrap();
let mut output_dir = dir.path().to_path_buf();
// `MAX_PATH` counts UTF-16 code units.
while output_dir
.join("openshell-ca.pem")
.as_os_str()
.encode_wide()
.count()
<= 260
{
output_dir.push("0123456789abcdef0123456789abcdef");
}

let (ca_path, bundle_path) = write_ca_files(&ca, &output_dir, "").unwrap();

assert_eq!(std::fs::read_to_string(&ca_path).unwrap(), ca.cert_pem());
assert!(
std::fs::read_to_string(&bundle_path)
.unwrap()
.contains(ca.cert_pem())
);
}

#[cfg(windows)]
#[test]
fn extended_length_path_keeps_the_path_when_canonicalize_fails() {
let dir = tempfile::tempdir().unwrap();
let resolved = extended_length_path(&dir.path().join("openshell-ca.pem"));
assert!(resolved.as_os_str().to_string_lossy().starts_with(r"\\?\"));

// The directory does not exist, so `canonicalize` fails.
let unresolved = dir.path().join("missing").join("openshell-ca.pem");
assert_eq!(extended_length_path(&unresolved), unresolved);
}

#[test]
fn durable_ca_round_trip_preserves_certificate_bytes() {
let generated = SandboxCa::generate().unwrap();
Expand Down
Loading