Skip to content

Commit 7f8e210

Browse files
authored
fix(sandbox): route console logs to stderr (#949)
Signed-off-by: John Myers <johntmyers@users.noreply.github.com> Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
1 parent 0d301d5 commit 7f8e210

3 files changed

Lines changed: 49 additions & 8 deletions

File tree

architecture/sandbox-custom-containers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ openshell sandbox create --from ./my-sandbox/ # directory with Dockerfile
9696

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

99-
- **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.
99+
- **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.
100100
- **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.
101101
- **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.
102102
- **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.

crates/openshell-sandbox/src/main.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ struct Args {
101101
fn main() -> Result<()> {
102102
let args = Args::parse();
103103

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

119-
let stdout_filter =
119+
let console_filter =
120120
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&args.log_level));
121121

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

177177
tracing_subscriber::registry()
178178
.with(
179-
OcsfShorthandLayer::new(std::io::stdout())
179+
OcsfShorthandLayer::new(std::io::stderr())
180180
.with_non_ocsf(true)
181-
.with_filter(stdout_filter),
181+
.with_filter(console_filter),
182182
)
183183
.with(
184184
OcsfShorthandLayer::new(file_writer)
@@ -192,14 +192,14 @@ fn main() -> Result<()> {
192192
} else {
193193
tracing_subscriber::registry()
194194
.with(
195-
OcsfShorthandLayer::new(std::io::stdout())
195+
OcsfShorthandLayer::new(std::io::stderr())
196196
.with_non_ocsf(true)
197-
.with_filter(stdout_filter),
197+
.with_filter(console_filter),
198198
)
199199
.with(push_layer)
200200
.init();
201201
// Log the warning after the subscriber is initialized
202-
warn!("Could not open /var/log for log rotation; using stdout-only logging");
202+
warn!("Could not open /var/log for log rotation; using stderr-only logging");
203203
(None, None)
204204
};
205205

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use std::process::Command;
5+
6+
#[test]
7+
fn startup_logs_go_to_stderr_not_stdout() {
8+
let output = Command::new(env!("CARGO_BIN_EXE_openshell-sandbox"))
9+
.arg("--")
10+
.arg("/usr/bin/printf")
11+
.arg("hello")
12+
.env("OPENSHELL_LOG_LEVEL", "info")
13+
.env_remove("RUST_LOG")
14+
.env_remove("OPENSHELL_POLICY_RULES")
15+
.env_remove("OPENSHELL_POLICY_DATA")
16+
.env_remove("OPENSHELL_SANDBOX_ID")
17+
.env_remove("OPENSHELL_ENDPOINT")
18+
.output()
19+
.expect("spawn openshell-sandbox");
20+
21+
assert!(
22+
!output.status.success(),
23+
"expected sandbox startup to fail without a policy source"
24+
);
25+
26+
let stdout = String::from_utf8_lossy(&output.stdout);
27+
let stderr = String::from_utf8_lossy(&output.stderr);
28+
29+
assert!(
30+
stdout.trim().is_empty(),
31+
"expected startup logs on stderr only, got stdout: {stdout}"
32+
);
33+
assert!(
34+
stderr.contains("Starting sandbox"),
35+
"expected startup log on stderr, got: {stderr}"
36+
);
37+
assert!(
38+
stderr.contains("Sandbox policy required"),
39+
"expected missing-policy error on stderr, got: {stderr}"
40+
);
41+
}

0 commit comments

Comments
 (0)