Commit Graph
481 Commits
Author SHA1 Message Date
jtoelke2 d331ed5117 feat(ci): add shadow-docker-build workflow for OS-49 Phase 3 (#964)
Signed-off-by: Jonas Toelke <jtoelke@nvidia.com>
2026-04-24 12:33:25 -07:00
Drew Newberry 25c827d271 test(e2e): fix filtered upload path assertion (#963) 2026-04-24 11:46:04 -07:00
jtoelke2 8a3c0b04ae feat(docker): add BINARY_SOURCE selector for prebuilt Rust binaries (#945)
* feat(docker): add BINARY_SOURCE selector for prebuilt Rust binaries

Signed-off-by: Jonas Toelke <jtoelke@nvidia.com>

* fix(docker): preserve exec bit on prebuilt binary COPY

Adds --chmod=755 to the COPY instructions in the scratch-based
prebuilt binary stages. Without this, binaries produced by PR 4a and
shuttled through actions/upload-artifact + download-artifact lose
their executable bit during the roundtrip, and the resulting image's
ENTRYPOINT fails at runtime.

Signed-off-by: Jonas Toelke <jtoelke@nvidia.com>

---------

Signed-off-by: Jonas Toelke <jtoelke@nvidia.com>
2026-04-24 13:31:24 -05:00
Piotr Mlocek df38d1f66f feat(ci): add Markdown and Mermaid linting (#933) 2026-04-24 11:27:02 -07:00
Adam Miller d44d8a1e27 feat: Openshell driver podman (#904)
* feat(podman): add Podman compute driver for rootless sandbox management

Adds openshell-driver-podman, a new compute driver that manages OpenShell
sandboxes as rootless Podman containers via the Podman REST API over a
Unix socket. Enables local workstation sandboxes without Kubernetes.

Driver features:
- Bridge networking with ephemeral host-port mapping for rootless SSH reachability
- Named volumes for workspace storage, Podman native health checks, GPU via CDI
- Supervisor binary sideloaded via image volume mount (BYOC-compatible)
- SSH handshake secret injected via Podman secrets API (not plaintext env)
- Typed ContainerSpec structs, input validation, and path-traversal guards
- Cgroups v2 required; fails fast on v1 hosts
- Bounded event stream buffer; watch stream reconnection handled by server watch_loop
- Graceful shutdown and standalone driver binary with gRPC bridge

Rootless-specific fixes:
- Skip drop_privileges when user namespace lacks SETUID/SETGID/DAC_READ_SEARCH caps
- Add /run/netns tmpfs mount for ip netns in rootless containers
- Use secret_env map (not secrets array) for env-var injection in libpod API
- Resolve SSH endpoint to 127.0.0.1:<host_port> instead of unreachable bridge IP

Server/sandbox hardening:
- Split loopback and link-local SSRF gates; Podman/VM drivers allow loopback
- Close SSRF bypass in SSH tunnel Host path by resolving DNS before connecting
- Prevent OPENSHELL_* env var override by user-supplied spec environment maps
- Disable SQLite pool idle_timeout/max_lifetime for in-memory databases
- Emit deleted_event on 404-during-inspect instead of regressing sandbox phase
- Key delete cleanup by stable sandbox_id to survive container label drift

CLI fixes:
- Restore --name as a named flag on sandbox create (not positional)
- Fix exec command arg parsing to not consume sandboxed-command flags
- Propagate SSH verbosity via OPENSHELL_SSH_LOG_LEVEL

Build tooling:
- Add tasks/scripts/container-engine.sh: auto-detects Podman or Docker, exposes
  unified ce_* helpers; all build/cluster/VM scripts updated to use it
- Add docker:build:supervisor mise task for standalone supervisor image
- Add openshell-driver-podman to Dockerfile.images pre-fetch/build stages
- Add e2e/rust/e2e-podman.sh and e2e:podman mise task for full lifecycle testing

Signed-off-by: Adam Miller <admiller@redhat.com>

* fix(driver-podman): derive grpc endpoint from server bind port

When a user starts the gateway on a non-default port (e.g. --port 8081),
sandbox containers were receiving OPENSHELL_ENDPOINT pointing at the
default port 8080. The driver's auto-detection fallback read
OPENSHELL_BIND_ADDRESS from the environment, which was stale or unset,
and fell back to DEFAULT_SERVER_PORT.

Add gateway_port to PodmanComputeConfig and thread config.bind_address.port()
from the server into the driver so the fallback uses the actual listening
port. Remove the OPENSHELL_BIND_ADDRESS env var read and the
extract_port_from_bind_address helper which are no longer needed.

Add --gateway-port / OPENSHELL_GATEWAY_PORT to the standalone driver
binary for parity when the driver is run outside the embedded server path.

Signed-off-by: Adam Miller <admiller@redhat.com>

* fix(driver-podman): address PR feedback on env test safety and cluster DNS docs

Replace hand-rolled unsafe TempEnvVar RAII guard with temp_env::with_vars
and a static ENV_LOCK mutex, fixing a data race in parallel test execution.
The prior safety comment incorrectly claimed Cargo runs tests single-threaded.

Update debug-openshell-cluster skill to accurately document the DNS proxy
strategy (setup_dns_proxy + public DNS fallback) and clarify the separation
between cluster DNS and sandbox agent DNS enforcement.

Signed-off-by: Adam Miller <admiller@redhat.com>

* fix(e2e): resolve CI failures in auth timeout, test harness, and formatting

- Short-circuit browser_auth_flow when OPENSHELL_NO_BROWSER=1 instead
  of waiting the full 120s AUTH_TIMEOUT for a callback that never arrives
- Add timeout to SandboxGuard::create() and create_with_upload() to
  prevent indefinite hangs (matches create_keep() which already had one)
- Add missing '--' separator in no_proxy test before command args
- Add #![cfg(feature = "e2e")] gate to sandbox_lifecycle.rs
- Run cargo fmt on openshell-driver-podman
- Refine cluster DNS docs for Podman in debug-openshell-cluster skill

Signed-off-by: Adam Miller <admiller@redhat.com>

* refactor(server): remove allows_loopback_endpoints from ComputeRuntime

SSRF protection is now handled at the network and proxy layers
(openshell-core net.rs, openshell-sandbox proxy.rs) rather than
requiring per-driver flags on ComputeRuntime. Update architecture
docs to reflect supervisor relay SSH transport and add rootless
networking deep-dive.

Signed-off-by: Adam Miller <admiller@redhat.com>

---------

Signed-off-by: Adam Miller <admiller@redhat.com>
2026-04-24 10:30:14 -07:00
jtoelke2 77a88c313b fix(ci): partition GHA sccache cache per arch in shadow spike (#961)
Without a per-arch SCCACHE_GHA_VERSION, both matrix jobs
(linux-amd64-cpu8 and linux-arm64-cpu8) target the same GHA cache
key. Run 1 on 2026-04-24 showed this concretely: arm64 started
first and landed 575/1064 cache writes; amd64 started ~1 min later
and every one of its 1062 writes failed, leaving the cache with no
amd64 entries at all.

Partitioning by matrix.runner ensures each arch gets its own
cache namespace, letting us see real warm-cache behavior on the
subsequent Run 2-4 dispatches.

Signed-off-by: Jonas Toelke <jtoelke@nvidia.com>
2026-04-24 11:39:09 -05:00
John T. MyersandJohn Myers 8cf5ebdc89 test(e2e): fix gitignore upload assertion path (#962)
Signed-off-by: John Myers <johntmyers@users.noreply.github.com>
Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
2026-04-24 09:26:50 -07:00
Drew Newberry a34b25a5ba test(e2e): fix rust upload path assertions (#960)
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
2026-04-24 08:28:40 -07:00
Derek Carr 87f50f5e5d fix(e2e): add /dev/urandom to provider test sandbox policy (#948)
Python runtime requires /dev/urandom access during initialization
to seed the hash randomizer. The _default_policy() in provider tests
was missing this path, causing exec_python tests to fail with:
'Fatal Python error: _Py_HashRandomization_Init: failed to get
random numbers to initialize Python'.

Add /dev/urandom to read_only paths to match the policy used in
test_sandbox_policy.py, allowing Python to initialize successfully.

Signed-off-by: Derek Carr <decarr@redhat.com>
2026-04-24 07:51:04 -07:00
John T. MyersandJohn Myers 7f8e2109e3 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>
2026-04-24 07:49:49 -07:00
mjamiv 0d301d578f fix(cli): preserve directory basename when uploading to sandbox (#952)
Previously `openshell sandbox upload <local-dir> <remote-dir>` flattened
the local directory's contents into the remote destination, dropping the
source directory's basename. This diverged from the standard `scp -r` /
`cp -r` semantics that operators expect from any *nix-style transfer
tool, and could silently shadow files at the destination.

Now an upload of `foo` to `/sandbox/dest/` lands at `/sandbox/dest/foo/`,
matching `scp -r foo remote:/sandbox/dest/` and `cp -r foo /sandbox/dest/`.
Paths without a meaningful basename (`.`, `/`) preserve the legacy flat
extraction.

Closes #885
2026-04-23 22:49:15 -07:00
jtoelke2 a4dfa5ad78 feat(ci): add driver input to setup-buildx action (#941)
Signed-off-by: Jonas Toelke <jtoelke@nvidia.com>
2026-04-23 22:58:31 -05:00
jtoelke2 ef2d993896 fix(ci): expose GHA sccache env in shadow-shared-cpu-spike (#950)
Signed-off-by: Jonas Toelke <jtoelke@nvidia.com>
2026-04-23 22:58:21 -05:00
jtoelke2 75b880b625 chore(ci): add ARC baseline collector for OS-49 runner migration (#927)
Signed-off-by: Jonas Toelke <jtoelke@nvidia.com>
2026-04-23 16:52:47 -05:00
jtoelke2 0a09404c1d feat(ci): add shadow-shared-cpu-spike workflow for OS-49 Phase 2 (#934)
Signed-off-by: Jonas Toelke <jtoelke@nvidia.com>
2026-04-23 16:51:47 -05:00
Taylor Mutch 550c6e46cb chore(helm): remove unused ClusterRole and ClusterRoleBinding (#943)
* chore(helm): remove unused cluster rule from helm chart
2026-04-23 13:52:27 -07:00
Piotr Mlocek ab3f3e033d fix(ci): post E2E Gate check to the PR when workflow_run fires (#938) 2026-04-23 13:02:56 -07:00
Seth Jennings 3b7d30934e feat(server): add Prometheus metrics infrastructure and gRPC/HTTP request metrics (#920)
* feat(server): add Prometheus metrics infrastructure and gRPC/HTTP request metrics

Add metrics exposition via a dedicated server port (--metrics-port,
default disabled) following the same optional-listener pattern as the
health port. The metrics crate facade records counters and histograms
in the MultiplexedService hot path, and a PrometheusHandle renders them
at GET /metrics on the dedicated port.

Metrics added:
- openshell_server_grpc_requests_total (counter: method, code)
- openshell_server_grpc_request_duration_seconds (histogram: method, code)
- openshell_server_http_requests_total (counter: path, status)
- openshell_server_http_request_duration_seconds (histogram: path, status)

* feat(helm): add metrics port to openshell-server helm chart

Expose the Prometheus metrics endpoint in the helm chart by adding
service.metricsPort (default 9090) to values, the --metrics-port arg
and container port to the statefulset, and a metrics service port.
Set metricsPort to 0 to disable.
2026-04-23 12:53:13 -07:00
Piotr Mlocek 9bc2e2cc31 fix(ci): rename mise --no-prepare to --no-deps (#942)
mise 2026.4.18 (bumped in #929) renamed the `--no-prepare` flag to
`--no-deps`. Update workflow invocations to match.
2026-04-23 11:26:24 -07:00
Florent BENOIT b19a3dc681 chore(mise): replace deprecated ubi: prefix by github: prefix (#923) 2026-04-23 10:59:55 -07:00
Seth Jennings 8405ceaa6f fix(skills): remove --assignee @me from gh pr/issue create commands (#937)
Most contributors cannot assign PRs or issues to themselves due to
repository settings. Remove the --assignee flag from skill templates
to prevent gh CLI errors.
2026-04-23 09:49:18 -07:00
John T. Myers c5d5855210 fix(ci): bump helm to 4.1.4 to address plugin vulnerabilities (#928)
Clears four High-severity container findings in the CI image, which
installs helm via mise.toml:

  - CVE-2026-35204 / GHSA-vmx8-mqv2-9gmg (plugin path traversal)
  - CVE-2026-35205 / GHSA-q5jf-9vfq-h4h7 (plugin .prov verification fails open)

Both are fixed in helm 4.1.4.

Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>
2026-04-23 09:38:01 -07:00
John T. Myers c6f579279e fix(ci): bump ci-image tooling versions to address vendored CVEs (#929)
Bumps the CI image tool pins to clear High-severity container findings
flagged by nSpect against ghcr.io/nvidia/openshell/ci. These tools
vendor the Go modules reported in the nSpect tracker.

Changes:
  - DOCKER_VERSION  29.3.1     -> 29.4.1
  - BUILDX_VERSION  v0.32.1    -> v0.33.0  (bundles buildkit v0.29.0,
                                            supersedes 0.28.1 fix)
  - GH_VERSION      2.74.1     -> 2.91.0
  - MISE_VERSION    v2026.3.13 -> v2026.4.18

Covers (via vendored deps in the above binaries):
  - GHSA-p77j-4mvh-x3m3  grpc-go authorization bypass
  - GHSA-4c29-8rgm-jvjj  BuildKit malicious-frontend file escape
  - GHSA-4vrq-3vrq-g6gg  BuildKit Git subdir access to restricted files
  - GHSA-9h8m-3fm2-qjrq  OpenTelemetry Go SDK PATH hijacking
  - GHSA-92mm-2pjq-r785  hashicorp/go-getter arbitrary file reads
  - GHSA-78h2-9frx-2jm8  go-jose JWE decryption panic
  - GHSA-4qg8-fj49-pxjh  sigstore timestamp-authority excessive memory
  - GHSA-x744-4wpc-v9h2  Moby AuthZ plugin bypass

Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>
2026-04-23 09:37:08 -07:00
Piotr Mlocek 89dd10bd4e fix(ci): e2e gate must verify work actually ran, not just top-level success (#926) 2026-04-23 08:28:27 -07:00
Piotr Mlocek d0a29b64eb fix(driver-vm): preflight supervisor cross-compile toolchain in start.sh (#931) 2026-04-23 08:27:27 -07:00
Drew Newberry 4483c860e0 feat(server,driver-vm,e2e): gateway-owned readiness + VM compute driver e2e (#901) v0.0.36 2026-04-22 16:42:35 -07:00
Piotr Mlocek 30ddca42d3 ci(e2e): enable E2E to run on external forks throught the copy-pr-bot flow (#922) 2026-04-22 14:15:10 -07:00
Tinson Lai 2f8e8ac311 fix(sandbox): inject GIT_SSL_CAINFO so git clone trusts the sandbox CA (#918)
Ubuntu Noble's git links against libcurl-gnutls, which does not read
SSL_CERT_FILE. Without GIT_SSL_CAINFO, `git clone` over HTTPS fails
inside every community sandbox image with "server certificate
verification failed".

Add GIT_SSL_CAINFO to tls_env_vars pointing at the same combined CA
bundle already used by CURL_CA_BUNDLE / REQUESTS_CA_BUNDLE.

Fixes #790
Fixes NVIDIA/NemoClaw#2270
Fixes NVIDIA/NemoClaw#1828

Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
2026-04-22 13:35:21 -07:00
John T. Myers f954e5927a fix(sandbox): resolve sandbox host aliases in SSRF checks (#912)
Closes #879

Resolve policy hostnames against the sandbox's /etc/hosts before DNS so Kubernetes hostAliases are visible to the proxy while keeping the existing allowed_ips SSRF enforcement semantics intact.

Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>
2026-04-22 12:37:32 -07:00
John T. Myers e28ca07863 fix(sandbox): preserve explicit read-only baseline paths (#910)
Closes #905

Skip proxy baseline read-write enrichment when a path is already explicitly configured as read-only, add regression coverage for the proto and local policy code paths, and document the precedence rule in the sandbox architecture docs.

Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>
2026-04-22 12:18:24 -07:00
Trevor McKay 78b685ed85 feat: add configurable timeout for image transfer to gateway containerd (#914)
the transfer of very large sandbox images to containerd can
timeout depending on the size of the image and the speed of
the local host.

Made-with: Cursor
2026-04-22 08:13:27 -07:00
Taylor Mutch cbcc4b7ee0 feat(server): allow disabling health check listener (#915) 2026-04-22 07:57:53 -07:00
Seth Jennings 42c3cf635d fix(k8s-driver): use dedicated kube client without read_timeout for watches (#907)
The 30s read_timeout on the shared kube client was killing the
long-lived watch streams during idle periods, causing a reconnect
cycle every 30 seconds. Use a separate client with no read_timeout
for watch_sandboxes so the streams stay open indefinitely.
v0.0.35
2026-04-21 13:57:08 -07:00
Seth Jennings bd113957c3 feat(server): serve health endpoints on separate unauthenticated port (#903)
Move /health, /healthz, and /readyz to a dedicated plaintext HTTP port
(default 8081) so Kubernetes probes work without mTLS client certificates.

- Add health_bind_address to Config with --health-port CLI arg
- Spawn standalone axum::serve for health_router on the health port
- Remove health routes from the main multiplexed HTTP router
- Update Helm statefulset probes from tcpSocket to httpGet on health port
- Fix cluster-healthcheck.sh to open/close TCP without sending data,
  avoiding InvalidContentType TLS errors in the gateway log
2026-04-21 13:26:09 -07:00
Seth Jennings ba56206f6a feat(server): add request-level logging via tower-http TraceLayer (#895)
Wrap gRPC and HTTP services with TraceLayer to log method, path,
status, and latency for every request. Health/readiness probe
endpoints are logged at debug level to reduce noise.
2026-04-21 10:38:43 -07:00
Piotr Mlocek a6d45528c1 feat(server,sandbox): supervisor-initiated SSH connect and exec over gRPC-multiplexed relay (#867) 2026-04-21 08:38:18 -07:00
John T. Myers c960d480fd fix(sandbox): canonicalize HTTP request-targets before L7 policy evaluation (#878)
* fix(sandbox): canonicalize HTTP request-targets before L7 policy evaluation

The L7 REST proxy evaluated OPA path rules against the raw request-target
and forwarded the raw header block byte-for-byte to the upstream. The
upstream normalized `..`, `%2e%2e`, and `//` before dispatching, so a
compromised agent inside the sandbox could bypass any path-based allow
rule by prefixing the blocked path with an allowed one (e.g.
`GET /public/../secret` matches `/public/**` at the proxy but the
upstream serves `/secret`). The inference-routing detection had the same
normalization gap via `normalize_inference_path`, which only stripped
scheme+authority and preserved dot-segments verbatim.

- Add `crates/openshell-sandbox/src/l7/path.rs` with
  `canonicalize_request_target`. Percent-decodes (with uppercase hex
  re-emission), resolves `.` / `..` segments per RFC 3986 5.2.4,
  collapses doubled slashes, strips trailing `;params`, rejects
  fragments / raw non-ASCII / control bytes / encoded slashes (unless
  explicitly enabled per-endpoint), and returns a `rewritten` flag so
  callers know when to rewrite the outbound request line.
- Wire the canonicalizer into `rest.rs::parse_http_request`. The
  canonical path is the `target` on `L7Request` that OPA sees, and
  when the canonical form differs from the raw input the request line
  is rebuilt in `raw_header` so the upstream dispatches on the exact
  bytes the policy evaluated.
- Canonicalize the forward (plain HTTP proxy) path at
  `proxy.rs`'s second L7 evaluation site. A non-canonical request-target
  is rejected with 400 Bad Request and an OCSF `NetworkActivity` Fail
  event rather than silently passed through.
- Replace the old `normalize_inference_path` body with a call to the
  canonicalizer. On canonicalization error the raw path is returned
  (so inference detection simply misses and the normal forward path
  handles and rejects).
- Document the invariant in `sandbox-policy.rego`: `input.request.path`
  is pre-canonicalized so rules must not attempt defensive matching
  against `..` or `%2e%2e`.
- Architecture doc (`architecture/sandbox.md`) lists the new module.
- 24 new unit tests cover dot segments, percent-encoded dot segments,
  double-slash collapse, encoded-slash reject/opt-in, null/control byte
  rejection, legitimate percent-encoded bytes round-tripping, mixed-case
  percent normalization, fragment rejection, absolute-form stripping,
  empty / length-bounded / non-ASCII handling, and regression payloads
  for the specific bypasses above.

Tracks OS-99.

* fix(sandbox): close forward-proxy parser-differential and wire allow_encoded_slash

Addresses two review findings on the L7 path canonicalization change.

1. `handle_forward_proxy` previously evaluated OPA on the canonical path
   but wrote the raw path to the upstream via `rewrite_forward_request`,
   leaving the parser-differential open for plain-HTTP forward-proxy
   traffic even though it was closed for the L7 tunnel path. `path` is
   now reassigned to the canonical form inside the L7 block before the
   outbound write, so the bytes dispatched to the upstream match what
   OPA evaluated. Covered by a new regression test against
   `rewrite_forward_request`.

2. `allow_encoded_slash` is now wired through the YAML policy schema,
   the proto `NetworkEndpoint` message, the Rego endpoint config, and
   the `L7EndpointConfig` → `RestProvider` canonicalization options.
   Operators can opt in per-endpoint for upstreams like GitLab that
   embed `%2F` in namespaced resource paths; the default remains
   strict. The `RestProvider` gains a constructor
   `RestProvider::with_options` so `relay_rest` constructs a provider
   with per-endpoint options while `relay_passthrough_with_credentials`
   retains strict defaults.

Integration tests:
- `parse_http_request_canonicalizes_target_and_rewrites_raw_header`
  drives a non-canonical request through the parser and asserts both
  `L7Request.target` (what OPA evaluates) and `raw_header` (what the
  upstream receives) are canonical.
- `parse_http_request_canonicalization_preserves_query_string`,
  `parse_http_request_leaves_canonical_input_byte_for_byte`,
  `parse_http_request_rejects_traversal_above_root`,
  `parse_http_request_preserves_http_10_version_on_rewrite`.
- `parse_http_request_accepts_encoded_slash_when_endpoint_opts_in` /
  `_rejects_encoded_slash_by_default` verify the
  `allow_encoded_slash` gate.
- `test_rewrite_forward_request_uses_canonical_path_on_the_wire`
  asserts the fix to the forward-proxy flow.
- `parse_l7_config_allow_encoded_slash_{defaults_false,opt_in}` verify
  the YAML/Rego wiring.
v0.0.34
2026-04-21 07:08:07 -07:00
Taylor Mutch 9ac725f00d fix(cli): sandbox get returns currently active runtime policy (#880)
* feat(cli): Allow printing only the policy when retrieving sandbox info

* feat(cli): Indicate policy source and revision when retrieving sandboxes

* cargo fmt
2026-04-20 16:44:48 -07:00
Drew Newberry b39f5aaa0c feat(install-vm): install gateway + vm driver, add --driver-dir resolution (#887) 2026-04-20 15:15:01 -07:00
John T. Myers 8a813aba49 fix(sandbox): apply supervisor seccomp prelude (#891)
* fix(sandbox): apply supervisor seccomp prelude

Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>

* fix(sandbox): delay supervisor prelude until bootstrap

---------

Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>
2026-04-20 13:09:23 -07:00
John T. Myers 7a0a3d0cce fix(cli,tui): escape and validate SSH session response fields (#876)
The CLI and TUI build an SSH ProxyCommand string by interpolating fields
from CreateSshSessionResponse into a shell command. Three fields -
sandbox_id, token, and the assembled gateway_url (composed from
gateway_scheme, gateway_host, gateway_port, connect_path) - were not
passed through shell_escape, even though exe_command and gateway_name
were. The resulting string is handed to `ssh -o ProxyCommand=...` which
OpenSSH routes through `/bin/sh -c`, so a hostile or compromised gateway
could inject shell metacharacters and execute arbitrary commands under
the developer's workstation account.

- Add build_proxy_command in openshell-core::forward that wraps every
  interpolated value in shell_escape. Use it at all four ProxyCommand
  sites: openshell-cli/src/ssh.rs (ssh_session_config) and the three
  TUI sites in openshell-tui/src/lib.rs (shell connect, sandbox exec,
  port-forward reconciliation).
- Add validate_ssh_session_response in openshell-core::forward, called
  at each site immediately after response.into_inner(). Enforces a
  conservative charset per field (sandbox_id, token, gateway_host,
  gateway_scheme, gateway_port, connect_path, host_key_fingerprint) so
  malformed responses fail loudly at the gRPC trust boundary before
  shell escaping is attempted. Belt-and-suspenders with the escape.
- Harden render_ssh_config so `gateway` and `name` are shell-escaped
  even though validate_gateway_name currently gates `gateway`.
- Document the charset contract on CreateSshSessionResponse in
  proto/openshell.proto: servers must uphold these rules and clients
  reject responses that violate them.
- Add regression unit tests covering adversarial values in every field
  and a build_proxy_command test asserting no shell metacharacter
  remains outside single-quoted regions.

Tracks OS-100.
2026-04-20 11:38:20 -07:00
John T. Myers 40e9bf6feb feat(policy): add incremental sandbox policy updates (#860)
* feat(policy): add incremental sandbox policy updates

Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>

* docs(policies): expand incremental update guidance

Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>

* feat(policy): audit incremental updates in gateway logs

* docs(policy): quote glob specs in shell examples

---------

Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com>
2026-04-20 08:00:02 -07:00
Piotr Mlocek e39bb38040 test(sandbox): fix flaky arm64 procfs binary_path tests (#881) v0.0.33 2026-04-17 17:47:37 -07:00
mjamiv ae7e901000 fix(sandbox): strip " (deleted)" suffix from unlinked /proc/<pid>/exe paths (#844)
* fix(sandbox): strip " (deleted)" suffix from unlinked /proc/<pid>/exe paths

When a running binary is unlinked from its filesystem path — the common
case is a `docker cp` hot-swap of `/opt/openshell/bin/openshell-sandbox`
during the `cluster-deploy-fast` dev upgrade workflow — the Linux kernel
appends the literal string ` (deleted)` to the `/proc/<pid>/exe` readlink
target. The tainted `PathBuf` then flows into `collect_ancestor_binaries`
and on into `BinaryIdentityCache::verify_or_cache`, which tries to
`std::fs::metadata` the path. `stat()` fails with `ENOENT` because the
literal suffix isn't a real filesystem path, and the CONNECT proxy denies
every outbound request with:

    ancestor integrity check failed for \
      /opt/openshell/bin/openshell-sandbox (deleted): \
      Failed to stat ...: No such file or directory (os error 2)

Reproduced in production 2026-04-15: a cluster-deploy-fast-style hot-swap
of the supervisor binary caused every pod whose PID 1 held the now-deleted
inode to deny ALL outbound CONNECTs (slack.com, registry.npmjs.org,
169.254.169.254, etc.), breaking Slack REST delivery, npm installs, and
IMDS probes simultaneously. Existing pre-hot-swap TCP tunnels (e.g. Slack
Socket Mode WSS) kept working because they never re-evaluate the proxy.

Strip the suffix in `binary_path()` so downstream callers see the clean,
grep-friendly path. This aligns the cache key and log messages with the
original on-disk location.

Note: stripping the suffix does NOT by itself make the identity cache
tolerant of a legitimate binary replacement — `verify_or_cache` will now
`stat` and hash whatever currently lives at the stripped path, which is
the NEW binary, and surface a clearer `Binary integrity violation` error.
Fully unblocking the cluster-deploy-fast hot-swap workflow needs a
follow-up that either (a) reads running-binary content from
`/proc/<pid>/exe` directly via `File::open` (procfs resolves this to the
live in-memory executable even when the original inode has been unlinked),
or (b) keys the identity cache by exec dev+inode instead of path. Happy to
send that as a separate PR once the approach is decided — filing this
narrow fix first because it stands on its own: it fixes a concrete
misleading error and unblocks the obvious next step.

Added `binary_path_strips_deleted_suffix` test that copies `/bin/sleep`
to a temp path, spawns a child from it, unlinks the temp binary, verifies
the raw readlink contains the ` (deleted)` suffix, then asserts the public
API returns the stripped path.

Signed-off-by: mjamiv <michael.commack@gmail.com>

* fix(sandbox): narrow the " (deleted)" suffix strip and exercise it end-to-end

Address review feedback from @johntmyers on the initial version of this PR:

procfs::binary_path
- Only strip the kernel's " (deleted)" suffix when stat() on the raw
  readlink target reports NotFound. A live executable whose basename
  literally ends with " (deleted)" is now returned unchanged instead of
  being silently truncated, which matters because identity.rs hashes
  whatever this function returns as a trust anchor.
- Operate on raw bytes via OsStrExt, so filenames that are not valid
  UTF-8 still get exactly one suffix stripped. The previous
  strip_suffix-on-&str path skipped non-UTF-8 entirely and fell through
  to returning the tainted path.
- Expand the doc comment to describe both guardrails.

procfs tests
- binary_path_preserves_live_deleted_basename: copy /bin/sleep to a
  live file literally named "sleepy (deleted)", spawn it, and assert
  that the returned path still ends with " (deleted)".
- binary_path_strips_suffix_for_non_utf8_filename: exec a binary whose
  basename contains a 0xFF byte, unlink it, and assert that
  binary_path returns the stripped non-UTF-8 path. Writes the bytes
  with OpenOptions + sync_all + explicit drop so the write fd is fully
  released before exec() to avoid ETXTBSY under concurrent tests.

proxy: extract resolve_process_identity helper
- Pull the peer-resolution + TOFU verify + ancestor walk + cmdline
  collection block out of evaluate_opa_tcp into resolve_process_identity.
- Introduce IdentityError which carries the deny reason along with
  whatever partial identity data was resolved before the failure so
  evaluate_opa_tcp can thread that into ConnectDecision unchanged.
- evaluate_opa_tcp now calls the helper and proceeds straight to the
  OPA evaluate step; the surface visible to OPA and OCSF is unchanged.

proxy: end-to-end regression test for the hot-swap contract
- resolve_process_identity_surfaces_binary_integrity_violation_on_hot_swap
  stands up a real TcpListener, copies /bin/bash to a temp path,
  primes BinaryIdentityCache with that binary, spawns bash with a
  /dev/tcp one-liner that opens a real connection to the listener,
  and captures the peer's ephemeral port from accept().
- Simulates `docker cp` correctly: unlink the running binary (which
  persists via the child's exec mapping) and create a fresh file
  with different bytes at the same path. Writing in place is
  rejected by the kernel with ETXTBSY, so the old single-inode
  approach did not actually model the production failure mode.
- Asserts the error returned by resolve_process_identity contains
  "Binary integrity violation" (from BinaryIdentityCache) and does
  NOT contain "Failed to stat" or "(deleted)" — the pre-PR-#844
  failure mode. The binary field on the error is populated and is
  free of the tainted suffix.
- Skips cleanly if /bin/bash is not installed. Child process is
  always reaped before the assertion block so a failure does not
  leak a sleeping process.

---------

Signed-off-by: mjamiv <michael.commack@gmail.com>
2026-04-17 14:25:59 -07:00
Mrunal Patel 5c3015a689 docs(contributing): add bash shell setup example for mise (#877)
Signed-off-by: Mrunal Patel <mrunalp@gmail.com>
2026-04-17 14:06:29 -07:00
Miyoung Choi b7c763204d docs: refresh user-facing docs for recent sandbox and inference changes (#868)
* docs: refresh user-facing docs for recent sandbox and inference changes

- architecture: document system CA loading for upstream TLS, `tls: skip`
  as the opt-out, gateway state persistence across restarts, and OCSF
  structured logging surface.
- inference: document per-provider header allowlist, Authorization
  stripping, 120s streaming idle tolerance, and extended-thinking
  timeout guidance.
- manage-sandboxes: add "Execute a Command in a Sandbox" section for
  `openshell sandbox exec` with flag reference.
- security best practices: expand seccomp denylist (unconditional and
  conditional blocks), document two-phase Landlock probe, High-severity
  `landlock-unavailable` finding, and inference keep-alive closure.
- observability logging: document port in HTTP log URLs, `[reason:...]`
  denial suffixes, proxy 403/502 JSON error bodies, and Landlock
  CONFIG:ENABLED/CONFIG:OTHER events.

Signed-off-by: Miyoung Choi <miyoungc@nvidia.com>

* docs(architecture): tighten high-level architecture page

Trim implementation detail (CA bundle paths, deprecated TLS keys, SSH
handshake secret) from the high-level architecture page, fix accuracy
issues surfaced during deep audit, and expand uncommon acronyms on first
mention.

- Drop unsupported "cost-based routing" claim from Privacy Router row.
- Replace "brokers requests across the platform" with auth-boundary
  description.
- Add "inference" to Policy Engine constraint list per AGENTS.md.
- Expand Deny rule to include SSRF, blocked control-plane port, and L7
  deny paths in addition to deny-by-default.
- Switch Allow/Deny labels from hyphen to colon; remove em dashes and a
  double space.
- Expand LLM, SSRF, L7, TLS, CA, PEM, SSH, OCSF, and JSONL on first use.

Signed-off-by: Miyoung Choi <miyoungc@nvidia.com>
Made-with: Cursor

* docs: address audit feedback on refresh PR

- observability/logging: rewrite the allowed_ips paragraph after the
  Denial Reasons table; the previous wording said authors "can use"
  invalid entries while also stating they were rejected, which was
  contradictory and conflated load-time validation with the runtime
  per-CONNECT denial phrases the section documents.
- about/architecture: split compound sentences in the new Gateway
  Lifecycle and Observability sections so each clause stands alone.
- inference/about: drop the streaming-tolerance sentence from the prose
  paragraph since the dedicated Streaming reliability table row already
  covers it.

Signed-off-by: Miyoung Choi <miyoungc@nvidia.com>
Made-with: Cursor

* docs(architecture): address review feedback on architecture page

- Drop the Privacy Router row from the Components table; it is not
  yet a separately exposed customer-facing component.
- Update the page description and intro count to match the remaining
  three components (gateway, sandbox, policy engine).
- Split the policy decision into the three modes that the engine
  actually implements: Explicit Deny (deny rules and hardening rules,
  takes precedence), Allow, and Implicit Deny (no rule matched).

Signed-off-by: Miyoung Choi <miyoungc@nvidia.com>
Made-with: Cursor

* docs(architecture): convert policy decisions to a table

Promote the three policy decisions (Explicit Deny, Allow, Implicit
Deny) to a top-level table with Decision, When it applies, and
Outcome columns instead of a nested bulleted list under list item 5.
Top-level tables render reliably across markdown renderers, where
nested-in-list tables do not.

Signed-off-by: Miyoung Choi <miyoungc@nvidia.com>
Made-with: Cursor

* docs: repharse a bit

---------

Signed-off-by: Miyoung Choi <miyoungc@nvidia.com>
2026-04-17 13:15:59 -07:00
Miyoung Choi 2c9c146cb6 docs: fix TOC structure (#797)
* docs: fix TOC structure

* docs: fix wrong section title

* remove Home

* fix(fern): properly add the landing page
2026-04-17 13:15:29 -07:00
Drew Newberry e4d6f92d9b feat(vm): add standalone libkrun compute driver (#858) 2026-04-17 08:50:57 -07:00
John T. MyersandJohn Myers 4e8dbcfe5c fix(sandbox): harden seccomp, inference routing, and process limits (#869)
* fix(sandbox): block AF_NETLINK in seccomp unconditionally

Move AF_NETLINK to the unconditional socket-domain block list alongside
AF_PACKET, AF_BLUETOOTH, and AF_VSOCK. Previously it was only blocked in
NetworkMode::Block, leaving it accessible in Proxy mode where network
namespace isolation already scopes netlink to the sandbox's own veth —
making this a defense-in-depth hardening rather than a live exposure.

Closes OS-94

* fix(sandbox): scope inference.local interception to port 443

The pre-OPA interception for inference.local matched on hostname alone,
allowing any port to bypass OPA policy evaluation — including under
deny-all (network_policies: {}). Add a port check so only port 443
takes the interception path; all other ports on inference.local now
fall through to OPA and are subject to normal policy evaluation.

Closes OS-95

* fix(sandbox): enforce RLIMIT_NPROC to prevent fork bomb DoS

Set a hard limit of 512 processes per UID in harden_child_process(),
applied before privilege drop so the sandbox user cannot raise it.
Prevents unrestricted fork() from exhausting the process table — most
relevant for local dev mode where K8s pod cgroup pids.max is absent.

Closes OS-96

* chore(sandbox): fix rustfmt formatting for seccomp blocked_domains

---------

Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
2026-04-17 08:26:11 -07:00
Drew Newberry 3bc8e444b3 docs(rfc): adopt per-RFC folder structure (#870)
Each RFC now lives in its own folder (rfc/NNNN-short-name/) with
README.md as the main proposal. This gives each RFC room for diagrams,
images, and other supporting files without cluttering the top level.

Move 0000-template into the new layout and update rfc/README.md to
describe the convention.
v0.0.32
2026-04-16 22:00:36 -07:00