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-custom-containers.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ openshell sandbox create --from ./my-sandbox/ # directory with Dockerfile

The `openshell-sandbox` supervisor adapts to arbitrary environments:

- **Log file fallback**: Attempts to open `/var/log/openshell.log` for append; silently falls back to stdout-only logging if the path is not writable.
- **Log file fallback**: Attempts to open `/var/log/openshell.log` for append; if the path is not writable, the supervisor keeps console shorthand logging on stderr only.
- **Command resolution**: Executes the command from CLI args, then the `OPENSHELL_SANDBOX_COMMAND` env var (set to `sleep infinity` by the server), then `/bin/bash` as a last resort.
- **Startup seccomp prelude**: Before parsing CLI args or starting the async runtime, the supervisor sets `PR_SET_NO_NEW_PRIVS` and installs a narrow seccomp filter that blocks mount/remount, the new mount API syscalls, module loading, kexec, `bpf`, `perf_event_open`, and `userfaultfd`. This closes the privileged remount window while still leaving required child-setup syscalls such as `setns` available.
- **Network namespace**: Requires successful namespace creation for proxy isolation; startup fails in proxy mode if required capabilities (`CAP_NET_ADMIN`, `CAP_SYS_ADMIN`) or `iproute2` are unavailable. If the `iptables` package is present, the supervisor installs OUTPUT chain rules (LOG + REJECT) inside the namespace to provide fast-fail behavior (immediate `ECONNREFUSED` instead of a 30-second timeout) and diagnostic logging when processes attempt direct connections that bypass the HTTP CONNECT proxy. If `iptables` is absent, the supervisor logs a warning and continues — core network isolation still works via routing.
Expand Down
14 changes: 7 additions & 7 deletions crates/openshell-sandbox/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ struct Args {
fn main() -> Result<()> {
let args = Args::parse();

// Try to open a rolling log file; fall back to stdout-only logging if it fails
// Try to open a rolling log file; fall back to stderr-only logging if it fails
// (e.g., /var/log is not writable in custom workload images).
// Rotates daily, keeps the 3 most recent files to bound disk usage.
let file_logging = tracing_appender::rolling::RollingFileAppender::builder()
Expand All @@ -116,7 +116,7 @@ fn main() -> Result<()> {
(writer, guard)
});

let stdout_filter =
let console_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&args.log_level));

let runtime = tokio::runtime::Builder::new_multi_thread()
Expand Down Expand Up @@ -176,9 +176,9 @@ fn main() -> Result<()> {

tracing_subscriber::registry()
.with(
OcsfShorthandLayer::new(std::io::stdout())
OcsfShorthandLayer::new(std::io::stderr())
.with_non_ocsf(true)
.with_filter(stdout_filter),
.with_filter(console_filter),
)
.with(
OcsfShorthandLayer::new(file_writer)
Expand All @@ -192,14 +192,14 @@ fn main() -> Result<()> {
} else {
tracing_subscriber::registry()
.with(
OcsfShorthandLayer::new(std::io::stdout())
OcsfShorthandLayer::new(std::io::stderr())
.with_non_ocsf(true)
.with_filter(stdout_filter),
.with_filter(console_filter),
)
.with(push_layer)
.init();
// Log the warning after the subscriber is initialized
warn!("Could not open /var/log for log rotation; using stdout-only logging");
warn!("Could not open /var/log for log rotation; using stderr-only logging");
(None, None)
};

Expand Down
41 changes: 41 additions & 0 deletions crates/openshell-sandbox/tests/stdout_logging.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

use std::process::Command;

#[test]
fn startup_logs_go_to_stderr_not_stdout() {
let output = Command::new(env!("CARGO_BIN_EXE_openshell-sandbox"))
.arg("--")
.arg("/usr/bin/printf")
.arg("hello")
.env("OPENSHELL_LOG_LEVEL", "info")
.env_remove("RUST_LOG")
.env_remove("OPENSHELL_POLICY_RULES")
.env_remove("OPENSHELL_POLICY_DATA")
.env_remove("OPENSHELL_SANDBOX_ID")
.env_remove("OPENSHELL_ENDPOINT")
.output()
.expect("spawn openshell-sandbox");

assert!(
!output.status.success(),
"expected sandbox startup to fail without a policy source"
);

let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);

assert!(
stdout.trim().is_empty(),
"expected startup logs on stderr only, got stdout: {stdout}"
);
assert!(
stderr.contains("Starting sandbox"),
"expected startup log on stderr, got: {stderr}"
);
assert!(
stderr.contains("Sandbox policy required"),
"expected missing-policy error on stderr, got: {stderr}"
);
}
Loading