-
Notifications
You must be signed in to change notification settings - Fork 299
Description
Problem Statement
Custom container images that use FUSE filesystems (e.g., mounting a PostgreSQL database as a virtual filesystem) cannot
work inside OpenShell sandboxes because:
- /dev/fuse is not exposed into sandbox containers
- User processes have zero capabilities (no CAP_SYS_ADMIN for mount(2))
- ENTRYPOINT/CMD is replaced by the supervisor — image startup code never runs with privileges
This is by design — the sandbox security model correctly prevents user code from having elevated privileges. But FUSE mounts are safe once established: they're kernel-level mounts that any unprivileged process can read/write through normal filesystem operations.
Proposed Design
OpenShell should discover FUSE requirements by inspecting the container image's static artifacts — not by running image
code or trusting image-declared scripts.
Discovery (image inspection)
The supervisor already inspects images for:
- sandbox user existence (validate_sandbox_user())
- Policy file at /etc/openshell/policy.yaml (disk discovery fallback)
- Working directory
Add: inspect for a well-known declarative config file:
/etc/openshell/fuse.yaml
mounts:
-
binary: /usr/local/bin/openeral
args: ["mount", "/db"]
mount_point: /db
env_from: OPENERAL_DATABASE_URL
ready_check: mountpoint
read_only: true -
binary: /usr/local/bin/openeral
args: ["workspace", "mount", "default", "/home/agent"]
mount_point: /home/agent
env_from: OPENERAL_DATABASE_URL
ready_check: mountpoint
read_only: false
This is declarative data, not executable code. The supervisor validates it against an allowlist/schema before acting on
it.
Execution (supervisor-managed, pre-child)
During the privileged startup phase (between prepare_filesystem() and ProcessHandle::spawn()):
- Read /etc/openshell/fuse.yaml from the image
- Validate: binary must exist in the image, mount points must be in policy read_write or read_only paths
- Create /dev/fuse via mknod (supervisor has SYS_ADMIN)
- For each mount: spawn the FUSE daemon as a supervisor-managed background process (like proxy and SSH server)
- Wait for ready_check (mountpoint becomes active)
- Drop privileges, apply Landlock/seccomp, spawn child
- Child sees mounts as regular directories — zero capabilities needed
Security properties preserved
- No image code runs with privileges — the supervisor reads a declarative config and runs known binaries. The binary
itself runs unprivileged (FUSE daemons don't need root after initial mount(2) — but the supervisor handles the privileged
mount setup). - Actually: FUSE daemons DO need to open /dev/fuse and call mount. So the FUSE daemon processes run as supervisor-managed
services with limited elevated access (just /dev/fuse + mount), separate from the child process. - Landlock still applies to child — child can only access paths in policy
- No capability leakage — child process has zero capabilities
- Declarative, not executable — fuse.yaml is validated data, not a script
- Binary allowlisting — supervisor can verify binary SHA256 (TOFU pattern already exists in identity.rs)
Open questions for OpenShell maintainers
- Should the FUSE daemon run as root or as the sandbox user? (Root simplifies mount setup but widens attack surface.
Running as sandbox user requires fusermount3 setuid binary.) - Should fuse.yaml support env var passthrough from provider environment? (FUSE daemons often need credentials like
database URLs.) - Should there be an allowlist of approved FUSE binaries, or is binary-exists-in-image sufficient?
Alternatives Considered
Approach 1: --device CLI flag (like Docker's --device /dev/fuse)
Exposes the device node into the pod via Kubernetes hostPath. Problem: this only gives you the device — the child process
still needs CAP_SYS_ADMIN to call mount(2) on it. So you'd also need capability passthrough. That breaks the security
model — you're giving user code elevated privileges.
Approach 2: --mount host path (#500)
Mount a host-side FUSE filesystem into the sandbox. Problem: this requires running openeral on the host, not inside the
sandbox. The host doesn't have our PostgreSQL credentials, openeral binary, or knowledge of what to mount. The sandbox
becomes dependent on host-side infrastructure. Defeats the self-contained image model.
Approach 3: Policy-driven device creation + capability passthrough
Add devices to policy YAML, supervisor creates /dev/fuse and passes CAP_SYS_ADMIN to child. Problem: giving child
CAP_SYS_ADMIN is exactly what you identified as breaking OpenShell's purpose. Any capability leak to user code undermines
the sandbox.
Approach 4: Supervisor runs image ENTRYPOINT with privileges
Supervisor executes the image's ENTRYPOINT as root during setup, then spawns child unprivileged. Problem: you identified
this too — running arbitrary image code with admin is dangerous. A malicious image could do anything.
Approach 5: Declarative fuse.yaml + supervisor-managed daemons (proposed)
Image declares FUSE mounts as data. Supervisor reads it, validates it, and runs the FUSE daemons itself as managed
services (like it already manages proxy and SSH). Child gets zero capabilities.
Why this one wins:
- No capability leak — child has zero capabilities, identical to today
- No arbitrary code execution with privileges — supervisor reads declarative config, validates binaries exist, runs them
under its own management - Self-contained — everything is in the image, no host-side setup
- Follows existing patterns — proxy and SSH server are already supervisor-managed background services spawned before the
child - Inspectable — OpenShell can validate/audit the FUSE config before acting on it
The one weakness: the FUSE daemon binary (e.g., openeral) does run with enough privilege to open /dev/fuse and call
mount(2). But it's managed by the supervisor, not by user code — similar to how the proxy runs with network access that
the child doesn't have.
Agent Investigation
Existing patterns in OpenShell that this follows
┌────────────────────────────────────────────────────────┬────────────────────────────────────────────────────────────┐
│ Existing pattern │ This proposal │
├────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────┤
│ Proxy runs as supervisor-managed background service │ FUSE daemons run as supervisor-managed background services │
├────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────┤
│ SSH server spawned before child, managed by supervisor │ FUSE daemons spawned before child, managed by supervisor │
├────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────┤
│ prepare_filesystem() creates dirs as root │ Supervisor creates /dev/fuse as root │
├────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────┤
│ /etc/openshell/policy.yaml discovered from image disk │ /etc/openshell/fuse.yaml discovered from image disk │
├────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────┤
│ validate_sandbox_user() inspects image │ FUSE config validation inspects image │
├────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────┤
│ Zombie reaper manages orphaned children │ Supervisor manages FUSE daemon lifecycle │
└────────────────────────────────────────────────────────┴────────────────────────────────────────────────────────────┘
Implementation locations in OpenShell
┌──────────────────────────────────────────┬──────────────────────────────────────────────────────────────────────────┐
│ File │ Change │
├──────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
│ crates/openshell-sandbox/src/lib.rs │ Add FUSE discovery + daemon spawn between prepare_filesystem() and │
│ │ ProcessHandle::spawn() │
├──────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
│ crates/openshell-sandbox/src/fuse.rs │ FUSE config parsing, validation, daemon management │
│ (new) │ │
├──────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
│ crates/openshell-policy/src/lib.rs │ Validate fuse mount points against policy paths │
└──────────────────────────────────────────┴──────────────────────────────────────────────────────────────────────────┘
Checklist
- I've reviewed existing issues and the architecture docs
- This is a design proposal, not a "please build this" request