Skip to content
Open
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
52 changes: 52 additions & 0 deletions .agents/skills/debug-openshell-cluster/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,58 @@ openshell status
openshell logs <sandbox-name>
```

#### Corporate upstream proxy

When VM sandbox egress routes through a corporate HTTP forward proxy, the
operator-owned settings live under `[openshell.drivers.vm]` and the gateway
forwards them to the `openshell-driver-vm` subprocess as `--https-proxy`,
`--no-proxy`, `--proxy-auth-file`, `--proxy-auth-allow-insecure`,
`--proxy-connect-by-hostname`, and `--proxy-ca-bundle`. Both the gateway and
the driver validate them at startup, so any present-but-invalid value fails
closed with an error naming the key rather than reverting to a direct dial.
Confirm the configuration and the resulting driver argv first:

```bash
grep -A20 '^\[openshell.drivers.vm\]' <gateway.toml> | grep -E 'https_proxy|no_proxy|proxy_auth_file|proxy_auth_allow_insecure|proxy_connect_by_hostname|proxy_ca_bundle'
ps -o args= -p "$(pgrep -f openshell-driver-vm | head -n1)" | tr ' ' '\n' | grep -A1 -- '--proxy\|--https-proxy\|--no-proxy'
```

Reachability is the most common failure, and it depends on the VM backend.
On libkrun (non-GPU sandboxes) guest egress leaves through gvproxy, so a proxy
bound to the gateway host's loopback is **not** reachable at `127.0.0.1` from
inside the guest: it must be addressed as
`http://host.openshell.internal:<port>`, which gvproxy NATs from
`192.168.127.254` to the host's `127.0.0.1`. A `https_proxy` pointing at a
loopback URL produces policy-approved CONNECT attempts that time out while
public destinations still work.

GPU sandboxes run on QEMU/TAP, where no gateway-host proxy is reachable at
all: `host.openshell.internal` resolves to the TAP host address, and the
driver's nftables `input` chain accepts only the gateway port from the guest.
The driver rejects such a configuration at launch — a create failing with
`https_proxy ... addresses the gateway host, which a QEMU/TAP sandbox ...
cannot reach` means the proxy must move to an address routable from the
guest's masqueraded egress (or the sandbox must run without a GPU).

The settings reach the supervisor through a driver-written argument file in
the per-sandbox overlay, not through the guest environment. The credential and
CA bundle are staged into the same overlay at fixed guest paths. Inspect the
guest side from the VM console log, which records how many driver-supplied
arguments the init script read:

```bash
grep -E 'supervisor arguments from driver|supervisor argument list' <state_dir>/sandboxes/<id>/rootfs-console.log
grep -Ei 'upstream|connect|proxy' <state_dir>/sandboxes/<id>/rootfs-console.log | tail -n 40
```

`FATAL: supervisor argument list ... is not readable` or `FATAL: empty entry in
supervisor argument list` means the overlay is broken or was tampered with, and
the guest deliberately aborts rather than starting a supervisor with a
truncated egress configuration. If the guest logs no driver arguments at all
while `gateway.toml` sets `https_proxy`, the running driver predates the
configuration — check that the gateway spawned the driver binary you expect
(`[openshell.drivers.vm].driver_dir`).

## Common Failure Patterns

| Symptom | Likely cause | Check |
Expand Down
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions architecture/sandbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,35 @@ file and builds the `Proxy-Authorization: Basic` header; a credential that is
empty, contains control characters, or is not in `user:pass` form is fatal on
both sides.

The VM driver has no argv seam of its own: its guest init script runs as PID 1
and execs a fixed supervisor command line, and the libkrun and QEMU launch
backends both reach the supervisor through that script. Driver-owned
supervisor arguments therefore travel in a per-sandbox file the driver writes
into the overlay upperdir at a fixed guest path, one argument per line, which
the guest reads verbatim (no word splitting or globbing) and appends to every
supervisor exec. The file is written on **every** launch, including an empty
file when there is nothing to pass: the upperdir copy always shadows the
read-only image layer, so a sandbox image can neither supply its own
supervisor arguments by baking a file at that path nor disable the operator's
by omitting one. This mirrors the driver-authored `init.d` manifest, which
solves the same trust problem for guest init drop-ins.

A microVM has no bind mounts or container secrets, so the VM driver stages the
credential and the CA bundle into the per-sandbox overlay disk instead — the
credential root-only, the CA world-readable, both at fixed `/opt/openshell`
paths and both removed with the sandbox state directory. The consequence,
which differs from the Podman secret model, is that the credential is at rest
inside that overlay image on the gateway host; the per-sandbox gateway JWT
already travels the same path. Proxy reachability differs by VM backend. libkrun-backed
sandboxes egress through gvproxy, so a proxy on the gateway host's loopback is
reachable through the host alias `host.openshell.internal`, which gvproxy NATs
to the host's `127.0.0.1`. QEMU/TAP sandboxes (GPU) have no equivalent: that
alias resolves to the TAP host address, and the driver's nftables `input`
chain accepts only the gateway port from the guest, so no gateway-host proxy
is reachable. The driver rejects a gateway-host proxy URL on the QEMU path at
launch rather than producing CONNECT timeouts. The guest's gateway callback is
unaffected in both backends and never traverses the proxy.

For Kubernetes sandboxes, the operator configures a Secret name and key rather
than a gateway-host file path. Kubernetes projects that Secret only into the
container that runs network supervision. Proxy credential Secrets require the
Expand Down
3 changes: 3 additions & 0 deletions crates/openshell-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ serde_json = { workspace = true }
tracing = { workspace = true }
url = { workspace = true }
ipnet = "2"
rustls = { workspace = true }
rustls-pemfile = { workspace = true }
base64 = { workspace = true }
chrono = { version = "0.4", default-features = false, features = ["clock", "std"], optional = true }
reqwest = { workspace = true, features = ["blocking", "rustls-tls-native-roots"], optional = true }
Expand All @@ -53,6 +55,7 @@ protoc-bin-vendored = { workspace = true }

[dev-dependencies]
tempfile = "3"
rcgen = { workspace = true }

[lints]
workspace = true
29 changes: 29 additions & 0 deletions crates/openshell-core/src/container_paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,32 @@ pub const VM_GUEST_TLS_KEY_PATH: &str = "/opt/openshell/tls/tls.key";
pub const VM_GUEST_SANDBOX_TOKEN_PATH: &str = "/opt/openshell/auth/sandbox.jwt";
pub const VM_GUEST_INIT_DROPIN_DIR: &str = "/opt/openshell/init.d";
pub const VM_GUEST_INIT_DROPIN_MANIFEST: &str = "/opt/openshell/init.d.manifest";

/// Guest path for the corporate upstream-proxy credential in VM sandboxes.
///
/// The VM driver stages the `user:pass` credential here (mode `0600`,
/// root-only) inside the per-sandbox overlay upperdir, and passes only this
/// path on the supervisor's argv. A microVM has no bind mounts or container
/// secrets, so this is the same delivery the per-sandbox JWT already uses.
pub const VM_GUEST_UPSTREAM_PROXY_AUTH_PATH: &str = "/opt/openshell/auth/upstream-proxy";

/// Guest path for the corporate proxy CA bundle in VM sandboxes.
///
/// A CA certificate is not secret, so unlike the credential this is staged
/// world-readable. The supervisor trusts it for the handshake with an
/// `https://` proxy and for server certificates re-signed by a
/// TLS-intercepting proxy.
pub const VM_GUEST_PROXY_CA_PATH: &str = "/opt/openshell/tls/proxy-ca.pem";

/// Guest path for the driver-authored supervisor argument list in VM sandboxes.
///
/// Podman and Kubernetes build the supervisor's command line directly; the VM
/// guest init script execs a fixed argv, so driver-owned arguments travel
/// through this file instead. The driver writes it into the overlay upperdir
/// on every launch — empty when it has no arguments to pass — so a sandbox
/// image can neither forge entries nor shadow the driver's copy, and the
/// guest appends exactly what it finds there and nothing else.
pub const VM_GUEST_SUPERVISOR_ARGS_PATH: &str = "/opt/openshell/supervisor-args";
pub const VM_UMOCI_PATH: &str = "/opt/openshell/bin/umoci";
pub const VM_SANDBOX_OWNER_NORMALIZED_MARKER: &str = "/opt/openshell/.sandbox-owner-normalized";

Expand Down Expand Up @@ -103,6 +129,9 @@ mod tests {
VM_GUEST_SANDBOX_TOKEN_PATH,
VM_GUEST_INIT_DROPIN_DIR,
VM_GUEST_INIT_DROPIN_MANIFEST,
VM_GUEST_UPSTREAM_PROXY_AUTH_PATH,
VM_GUEST_PROXY_CA_PATH,
VM_GUEST_SUPERVISOR_ARGS_PATH,
VM_UMOCI_PATH,
VM_SANDBOX_OWNER_NORMALIZED_MARKER,
];
Expand Down
Loading
Loading