33 Commits
Author SHA1 Message Date
Drew Newberry ddb85b1704 feat(vm): add openshell-vm crate with libkrun microVM gateway (#611) 2026-04-08 22:00:01 -07:00
John T. Myers 77e55ea989 test(e2e): replace flaky Python live policy update tests with Rust (#742)
Remove test_live_policy_update_and_logs and
test_live_policy_update_from_empty_network_policies from the Python e2e
suite. Both used a manual 90s poll loop against GetSandboxPolicyStatus
that flaked in CI with 'Policy v2 was not loaded within 90s'.

Add e2e/rust/tests/live_policy_update.rs with two replacement tests
that exercise the same policy lifecycle (version bumping, hash
idempotency, policy list history) through the CLI using the built-in
--wait flag for reliable synchronization.
2026-04-02 15:06:32 -07:00
Drew Newberry e837849009 feat(bootstrap): resume gateway from existing state and persist SSH handshake secret (#488) 2026-04-02 09:50:10 -07:00
Evan LezarandPiotr Mlocek 047de66b22 feat(bootstrap,cli): switch GPU injection to CDI where supported (#495)
* feat(bootstrap): switch GPU injection to CDI where supported

Use an explicit CDI device request (driver="cdi", device_ids=["nvidia.com/gpu=all"])
when the Docker daemon reports CDI spec directories via GET /info (SystemInfo.CDISpecDirs).
This makes device injection declarative and decouples spec generation from consumption.

When the daemon reports no CDI spec directories, fall back to the legacy NVIDIA device
request (driver="nvidia", count=-1) which relies on the NVIDIA Container Runtime hook.
Failure modes for both paths are equivalent: a missing or stale NVIDIA Container Toolkit
installation will cause container start to fail.

CDI spec generation is out of scope for this change; specs are expected to be
pre-generated out-of-band, for example by the NVIDIA Container Toolkit.

---------

Signed-off-by: Evan Lezar <elezar@nvidia.com>
Co-authored-by: Piotr Mlocek <pmlocek@nvidia.com>
2026-03-31 08:23:52 -07:00
John T. MyersandJohn Myers 758c62d18d fix(sandbox): handle per-path Landlock errors instead of abandoning entire ruleset (#677)
* fix(sandbox): handle per-path Landlock errors instead of abandoning entire ruleset

A single missing path (e.g., /app in containers without that directory)
caused PathFd::new() to propagate an error out of the entire Landlock
setup closure. Under BestEffort mode, this silently disabled all
filesystem restrictions for the sandbox.

Changes:
- Extract try_open_path() and classify_path_error() helpers that handle
  PathFd failures per-path instead of per-ruleset
- BestEffort mode: skip inaccessible paths with a warning, apply
  remaining rules
- HardRequirement mode: fail immediately on any inaccessible path
- Add zero-rule safety check to prevent applying an empty ruleset that
  would block all filesystem access
- Pre-filter system-injected baseline paths (e.g., /app) in enrichment
  functions so missing paths never reach Landlock
- Add unit tests for try_open_path, classify_path_error, and error
  classification for ENOENT, EACCES, ELOOP, ENAMETOOLONG, ENOTDIR
- Update user-facing docs and architecture docs with Landlock behavior
  tables, baseline path filtering, and compatibility mode semantics
- Fix stale ABI::V1 references in docs (code uses ABI::V2)

Closes #664

* fix(sandbox): use debug log for NotFound in Landlock best-effort mode

NotFound errors for stale baseline paths (e.g. /app persisted in the
server-stored policy but absent in this container) are expected in
best-effort mode. Downgrade from warn! to debug! so the message does
not leak into SSH exec stdout (the pre_exec hook inherits the tracing
subscriber whose writer targets fd 1).

Genuine errors (permission denied, symlink loops, etc.) remain at warn!
for operator visibility.

Also move custom_image e2e marker from /opt to /etc (a Landlock baseline
read-only path) since the security fix now properly enforces filesystem
restrictions.

---------

Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
2026-03-30 13:19:19 -07:00
John T. MyersandJohn Myers 0832f11a66 fix(e2e): add uv-managed python binary glob to forward proxy L7 test (#686)
The base sandbox image installs Python via uv at
/sandbox/.uv/python/*/bin/python*, but the proxy resolves binary
identity via /proc/PID/exe (the real path, not the symlink). The test
policy only listed /usr/bin/python* and /usr/local/bin/python*, so OPA
denied the connection at L4 before L7 evaluation could run.

Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
2026-03-30 11:58:45 -07:00
LateNightHackathonandlatenighthackathon 94fbb643b3 fix(proxy): add L7 inspection to forward proxy path (#666)
* refactor(l7): export evaluate_l7_request for cross-module use

Make evaluate_l7_request() public so the forward proxy path can
evaluate individual requests against L7 policy without going
through the full relay_with_inspection() loop.

Signed-off-by: latenighthackathon <latenighthackathon@users.noreply.github.com>

* fix(proxy): add L7 inspection to forward proxy path

The forward proxy previously rejected all requests to endpoints with
L7 rules (blanket 403), forcing clients through the CONNECT tunnel.
This meant policies like read-only (allow GET, block POST) had no
effect on plain http:// requests through the forward proxy.

Replace the blanket rejection with actual L7 evaluation:
- Query L7 config for the endpoint (same as before)
- Clone the OPA engine and evaluate the request method/path
- Allow if L7 policy permits, deny with 403 if enforcement is enforce
- Audit mode: log but allow (matching CONNECT path behavior)
- Fail-closed: deny on evaluation errors

The forward proxy uses Connection: close (one request per connection),
so a single evaluation suffices — no relay loop needed.

Update e2e tests to validate the new behavior:
- GET /allowed → 200 (L7 policy allows)
- POST /allowed → 403 (L7 policy denies, enforcement: enforce)

Update security-policy.md to reflect the new forward proxy L7 behavior.

Closes #643

Signed-off-by: latenighthackathon <latenighthackathon@users.noreply.github.com>

* style(proxy): apply cargo fmt formatting

Signed-off-by: latenighthackathon <latenighthackathon@users.noreply.github.com>

---------

Signed-off-by: latenighthackathon <latenighthackathon@users.noreply.github.com>
Co-authored-by: latenighthackathon <latenighthackathon@users.noreply.github.com>
2026-03-30 09:24:38 -07:00
John T. Myers a831a8921b feat(settings): gateway-to-sandbox runtime settings channel (#474)
* feat(gateway/sandbox): add global and sandbox runtime settings flow
2026-03-20 14:08:57 -07:00
Drew Newberry 5565a8bba3 fix(cli): suppress browser popup during auth via OPENSHELL_NO_BROWSER env var (#419) 2026-03-18 09:38:51 -07:00
Piotr Mlocek 34804e1e83 fix(e2e): replace Docker Hub images in E2E tests to avoid rate limits (#369) 2026-03-16 13:51:32 -07:00
John T. Myers 647b7947f6 fix: security hardening from aardvark/codex scanner findings (#352)
* fix(sandbox): prevent overread request smuggling in L7 REST parser

The HTTP/1.1 parser used a 1024-byte read buffer that could capture
bytes from a pipelined second request. Those overflow bytes were
forwarded upstream as body overflow without L7 policy evaluation,
enabling request smuggling that bypasses per-request method/path
enforcement.

Replace multi-byte read with byte-at-a-time read_u8 that stops exactly
at the CRLFCRLF header terminator. Add regression test proving two
pipelined requests are parsed independently.

Refs: #350

* fix(server): harden sandbox TLS secret volume permissions to 0400

Kubernetes secret volumes default to 0644, allowing the unprivileged
sandbox user to read the mTLS client private key via the Landlock
baseline /etc read set. A compromised sandbox could use the key to
impersonate the control-plane client.

Set defaultMode to 256 (octal 0400, owner-read only) on both the
default and custom pod template paths. The supervisor reads TLS
materials as root before forking, so this does not affect normal
operation.

Refs: #350

* fix(sandbox): stop using cmdline paths for binary policy matching

/proc/pid/cmdline is fully attacker-controlled (argv[0] can be set to
any string via execve) and had no integrity verification, unlike
exec.path and ancestors which are kernel-managed and get TOFU/SHA256
checks. The Rego policy used cmdline_paths as a grant-access signal,
allowing any sandboxed process to claim the identity of an allowed
binary and bypass network restrictions.

Remove the cmdline exact-match rule and exclude cmdline_paths from the
glob-match rule. Only exec.path and exec.ancestors (from /proc/pid/exe)
are now used for binary identity. cmdline_paths remain in the OPA input
for deny-reason diagnostics only.

Refs: #350

* fix(sandbox): reject symlink and non-dir read_write paths before chown

The supervisor runs as root and calls chown on each read_write path.
Since chown follows symlinks, a malicious container image could place a
symlink (e.g. /sandbox -> /etc/shadow) to trick the supervisor into
transferring ownership of arbitrary files to the sandbox user.

Add symlink_metadata (lstat) check before chown to reject symlinks and
non-directory entries. The TOCTOU window is not exploitable because no
untrusted child process has been forked yet at this point.

Refs: #350

* fix(server): redact provider credentials in gRPC CRUD responses

Provider CRUD RPCs (create, get, list, update) returned full Provider
objects including plaintext credentials (API keys, secrets). Any
authenticated client -- including sandbox workloads running untrusted
code -- could read credentials for all providers.

Add redact_provider_credentials helper that clears the credentials map
before returning. Internal server paths (inference routing, sandbox env
injection) read from the store directly and are unaffected. Update
tests to verify redaction and assert persistence via direct store reads.

Refs: #350

* fix(sandbox): enforce non-root fallback when process user unset

drop_privileges silently returned Ok(()) when both run_as_user and
run_as_group were None, even when running as root. In local/dev mode
policies are loaded from disk without passing through the server-side
ensure_sandbox_process_identity normalization, so child processes could
retain root and all capabilities (SYS_ADMIN, NET_ADMIN).

When running as root with no process identity configured, fall back to
sandbox:sandbox instead of no-oping. Non-root runtimes are unaffected.

Refs: #350

* fix(sandbox): deny forward proxy for L7-configured endpoints

The forward proxy path only performed L4 (endpoint) and allowed_ips
checks. If an endpoint had L7 rules (method/path restrictions), a
sandboxed process could bypass them by using HTTP_PROXY with plain
http:// requests instead of CONNECT tunneling, since L7 inspection
only runs in the CONNECT path.

Add a guard in handle_forward_proxy that queries the endpoint's L7
config and returns 403 if any L7 rules are present, forcing traffic
through the CONNECT path where per-request inspection happens.

Refs: #350

* test(e2e): add regression test for forward proxy L7 bypass

Verifies that the forward proxy path (plain http:// via HTTP_PROXY)
returns 403 for endpoints with L7 rules configured, preventing
sandboxed processes from bypassing per-request method/path enforcement
by avoiding the CONNECT tunnel.

Refs: #350

* fix: update tests for cmdline path and TLS volume changes

Update OPA tests to verify cmdline_paths no longer grant access
(regression tests for the security fix). Fix formatting in TLS
volume test.

Refs: #350

* test(e2e): update provider e2e tests for credential redaction

Provider CRUD gRPC responses no longer include credential values.
Update three e2e tests to assert credentials are empty in responses.
Provider functionality is verified by existing e2e tests that check
env var injection into sandboxes (which read from the store directly).

Refs: #350

* chore: reformat for Rust 1.94 assert! macro style

* fix(sandbox): make drop_privileges tests root-aware for CI

CI runs as root but has no 'sandbox' user. The security fix correctly
errors when running as root with no process identity and no fallback
user available -- this is the intended behavior (refuse to run as root).
Update tests to expect that error in root-without-sandbox-user
environments instead of unconditionally asserting Ok.

Refs: #350

* fix(sandbox): allow non-directory read_write entries like /dev/null

The symlink guard incorrectly rejected all non-directory entries.
Character devices like /dev/null are legitimate read_write paths
used in sandbox policies. Only reject symlinks, which are the
actual attack vector for the chown privilege escalation.

Refs: #350
2026-03-16 07:55:19 -07:00
Piotr Mlocek 3a328be853 feat(inference): verify endpoints before saving routes (#291)
Closes #273

Verify inference endpoints synchronously on the server during set/update, expose a --no-verify escape hatch in the CLI and Python helper, and return actionable failures when validation does not pass.
2026-03-15 19:36:23 -07:00
Drew Newberry 9fdffc72b2 feat(bootstrap): add Docker preflight check before gateway startup (#321) 2026-03-15 09:45:19 -07:00
Piotr Mlocek 49d8aa42f3 test(e2e): run host alias checks from docker (#314) 2026-03-14 20:09:58 -07:00
Drew NewberryandPiotr Mlocek 83af7a245b feat(sandbox): inject host gateway hostAliases into sandbox pods (#306)
* feat(sandbox): inject host gateway hostAliases into sandbox pods

Sandbox pods running in the k3s cluster cannot resolve host.docker.internal
by default, preventing them from reaching services on the Docker host.

Detect the host gateway IP (default route) in the cluster entrypoint,
thread it through the Helm chart to the gateway server, and inject
hostAliases entries (host.docker.internal, host.openshell.internal)
into every sandbox pod spec. The injection is conditional -- when the
IP is empty (non-Docker deployments), no hostAliases are added.

---------

Co-authored-by: Piotr Mlocek <pmlocek@nvidia.com>
2026-03-14 16:54:03 -07:00
Piotr Mlocek 50ea7495c1 fix(sandbox): bypass proxy for localhost traffic (#290) 2026-03-13 15:30:08 -07:00
Drew Newberry 20c32716ad fix(bootstrap): detect missing sandbox supervisor binary during gateway health check (#281) 2026-03-13 15:07:50 -07:00
Piotr Mlocek 72e0268028 feat(sandbox): add gpu sandbox scheduling support (#257)
* feat(sandbox): add gpu sandbox scheduling support

Allow sandbox creation to request GPU resources explicitly or infer them from GPU image names. This wires GPU intent through bootstrap, validates gateway support, and adds dedicated GPU E2E coverage for follow-up cluster testing.
2026-03-13 11:32:43 -07:00
Drew Newberry fbd93a4632 refactor: rename navigator- crate prefix to openshell- (#277) 2026-03-13 02:02:18 -07:00
Drew Newberry 7b0a243304 ci: remove sandbox docker build from publish and e2e workflows (#275) 2026-03-13 00:56:07 -07:00
Piotr Mlocek 14e296d318 fix(cli): add --no-keep for ephemeral sandbox create cleanup (#258) 2026-03-12 17:18:39 -07:00
Drew Newberry 6a40f2bed8 feat(gateway): support adding remote and local gateways (#262) 2026-03-12 16:19:47 -07:00
Drew Newberry b9d10861b6 refactor(sandbox): move secrets to supervisor placeholders (#192) 2026-03-12 10:30:46 -07:00
Drew Newberry bcc6dad1d0 feat(cli): launch sandbox editors via managed ssh include (#226) 2026-03-11 20:06:38 -07:00
Drew Newberry 756950140c refactor(python): rename navigator module to openshell and migrate config to gateway paths (#220) 2026-03-10 22:24:04 -07:00
Drew Newberry 36e8241293 fix: switch community sandbox registry to GHCR and align TLS paths (#218) 2026-03-10 19:38:22 -07:00
Drew Newberry 984d1a6e5c chore: rename project from NemoClaw to OpenShell (#198) 2026-03-10 11:49:09 -07:00
Drew Newberry 1cf54ca055 feat(bootstrap): switch container registry from CloudFront CDN to GHCR with token auth (#167) 2026-03-09 23:46:23 -07:00
Drew Newberry a2de1f24e5 feat: add Cloudflare tunnel auth support (#178) 2026-03-09 18:39:42 -07:00
Drew Newberry 1e4faf7b7a feat(cli): auto-create providers for explicit --provider names that match a known type (#183)
When --provider <name> is passed and no provider with that name exists on
the server, the CLI now checks if the name is a recognized provider type
(e.g. claude, nvidia, github). If it is, the provider is auto-created
from local credentials using the same discovery flow as inferred providers.

If the name is not a recognized type, the CLI returns a clear error
instead of deferring validation to the server.

This eliminates the FailedPrecondition error that occurred when passing
--provider nvidia on a fresh cluster where no providers had been created
yet.
2026-03-09 14:36:01 -07:00
Piotr Mlocek 5177acca36 fix(cli): scope git-aware sandbox uploads to requested path (#171) 2026-03-07 19:08:30 -08:00
Drew Newberry 07b9d5d00e feat(cli): restructure CLI commands for simpler UX (#156) 2026-03-06 16:26:39 -08:00
Drew Newberry a8e9b43dcc refactor(e2e): replace bash e2e tests with Rust integration tests (#150) 2026-03-06 09:01:49 -08:00