Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ceac9bf
feat(sandbox,gateway): route sandbox egress through corporate HTTP proxy
feloy Jul 9, 2026
f3a4f29
fix(sandbox,podman): make corporate proxy routing operator-owned
feloy Jul 16, 2026
9f9b8b6
feat(sandbox,podman): deliver corporate proxy credentials via secret …
feloy Jul 16, 2026
5eb14cb
fix(sandbox,podman): fail closed on invalid upstream proxy configuration
feloy Jul 16, 2026
467356e
fix(sandbox,podman): finish the fail-closed upstream proxy credential…
feloy Jul 16, 2026
743b4e6
fix(sandbox,podman): close remaining fail-open upstream proxy config …
feloy Jul 16, 2026
064e695
fix(sandbox,podman): reject upstream proxy URLs with path, query, or …
feloy Jul 16, 2026
d0c84e8
fix(sandbox,podman): remove plain-HTTP upstream proxy support
feloy Jul 16, 2026
ed9d20b
fix(sandbox,podman): escape generated TOML and require explicit proxy…
feloy Jul 17, 2026
53357e4
fix(sandbox,podman): preserve tunneled bytes read with the CONNECT re…
feloy Jul 17, 2026
d63a90c
fix(sandbox,podman): gate cleartext proxy Basic auth behind an explic…
feloy Jul 17, 2026
eced5a5
fix(sandbox,podman): honor port qualifiers and resolved addresses in …
feloy Jul 17, 2026
7596917
fix(sandbox,podman): bind proxied CONNECT tunnels to validated addresses
feloy Jul 17, 2026
787c4d8
fix(sandbox,podman): reject empty port after bracketed IPv6 proxy host
feloy Jul 17, 2026
45ad870
fix(sandbox,podman): fall back across validated addresses in proxied …
feloy Jul 17, 2026
d4bc174
fix(sandbox,podman): strip new reserved proxy vars and complete docs/…
feloy Jul 17, 2026
4740e40
fix(sandbox,podman): cap each proxied CONNECT attempt within the shar…
feloy Jul 17, 2026
89b5472
fix(sandbox,podman): deliver corporate proxy config on the supervisor…
feloy Jul 17, 2026
3fb1c96
docs(sandbox): align proxy comments with the argv transport
feloy Jul 18, 2026
ffe6544
fix(sandbox): parse bracketed IPv6 authorities in client CONNECT targets
feloy Jul 18, 2026
5c80418
test(podman): cover proxy-auth secret cleanup across lifecycle failures
feloy Jul 18, 2026
e27ba05
docs: list corporate proxy keys in the Podman compute-driver overview
feloy Jul 18, 2026
b7606c1
test(sandbox): cover the SSRF-to-TLS composition across the proxy tunnel
feloy Jul 18, 2026
b2a4512
fix(sandbox,podman): bound proxy-auth reads, reject port 0, fix stale…
feloy Jul 18, 2026
0e083a4
fix(sandbox): open proxy-auth file non-blocking to reject FIFOs promptly
feloy Jul 18, 2026
8ad21ec
test(podman): cover corporate proxy egress across driver and supervisor
feloy Jul 19, 2026
f6cfcf4
test(podman): restart the gateway on the proxy-config panic path
feloy Jul 19, 2026
8ceea80
fix(supervisor-network): derive the loopback proxy bypass from resolv…
feloy Jul 19, 2026
47d47af
fix(podman): clean up proxy-auth secret when container already deleted
feloy Jul 21, 2026
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
1 change: 1 addition & 0 deletions Cargo.lock

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

64 changes: 64 additions & 0 deletions architecture/sandbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,70 @@ handled by the inference interception path:
External inference endpoints that do not use `inference.local` are treated like
ordinary network traffic and must be allowed by policy.

In proxy-required networks, the supervisor chains upstream TLS tunnels through
a corporate forward proxy with HTTP CONNECT instead of connecting directly,
once policy and SSRF checks pass. Only TLS (CONNECT) egress is chained:
plain-HTTP requests always dial the destination directly, because forwarding
plain HTTP through a proxy requires absolute-form request forwarding rather
than CONNECT tunneling and is out of scope. The proxy configuration is an
operator-owned boundary delivered on the supervisor's command line
(`--upstream-proxy` and friends) by the compute driver; sandbox and template
environment — and `ENV` values baked into the sandbox image — cannot
influence it, since none of these can alter the argv the driver sets. The
conventional `HTTPS_PROXY`/`HTTP_PROXY`/`NO_PROXY` variables a sandbox
controls are ignored on this path. Operator `NO_PROXY` destinations and
loopback always dial directly; add driver-injected host aliases (e.g.
`host.containers.internal`) to the operator `NO_PROXY` list when the corporate
proxy cannot reach the container host. `NO_PROXY` matching is port-aware and
resolution-aware: an entry with a `:port` qualifier only bypasses that port,
and IP/CIDR entries also match hostnames through their validated resolved
addresses, with the direct dial limited to the addresses the entry contains. Only `http://` proxy URLs in explicit
`http://host:port` form are supported — the scheme and port are both
required, and a path, query, or fragment is rejected. Local DNS resolution
and SSRF validation still run before the proxied dial, and the CONNECT
target sent to the corporate proxy is a validated resolved address, so the
proxy performs no DNS resolution of its own and the tunnel stays bound to
the answer that passed SSRF and `allowed_ips` validation. The hostname still
travels inside the tunnel (TLS SNI, application `Host`). In split-horizon
networks, point the gateway host at the corporate resolver so internal names
validate to their internal addresses; the `proxy_connect_by_hostname`
opt-in exists as a
last resort for proxies whose ACLs filter on hostnames and reject IP CONNECT
targets — with it, the proxy resolves the name itself and its ACLs become
the effective egress control for proxied TLS. (Resolving through the proxy's
own DNS view, e.g. DoH tunneled via CONNECT, is a possible future
enhancement and out of scope.) The workload child's proxy variables are
unaffected — they are always rewritten to point at the local policy proxy.

The configuration is fail-closed: a setting that is present but invalid — an
empty value, an unsupported or malformed proxy URL, an unreadable auth file,
a malformed credential, or an auth file or `NO_PROXY` list set while no proxy
URL is configured — is fatal to supervisor startup instead of being treated
as unset, so a misconfiguration can never silently degrade to direct dialing
or unauthenticated proxy access. Only an omitted argument means "no proxy".
The driver validates the same rules at sandbox-create time through
validators shared with the supervisor
(`openshell_core::driver_utils::parse_upstream_proxy_url` and
`parse_upstream_proxy_credential`).

Proxy credentials are never embedded in the URL: an inline `user:pass@` is
rejected because it would be stored in `gateway.toml` and exposed in container
metadata. Operators supply credentials via `proxy_auth_file`; the driver
stages them as a root-only secret mounted at a fixed path and passes only
that path on the supervisor's command line. The supervisor reads the
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 Basic header travels over the plain-TCP connection to the `http://` proxy,
so it is readable on the network path between sandbox host and proxy.
Configuring `proxy_auth_file` therefore requires the explicit opt-in
`proxy_auth_allow_insecure = true`. Both the
driver (at sandbox-create time) and the supervisor (at startup) reject an
auth file without the acknowledgement, and the acknowledgement without an
auth file, so credentials are never sent in cleartext without an explicit
operator decision.

## Credentials

Provider credentials are stored at the gateway and fetched by the supervisor at
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 @@ -28,6 +28,9 @@ base64 = { workspace = true }
chrono = { version = "0.4", default-features = false, features = ["clock", "std"], optional = true }
reqwest = { workspace = true, features = ["blocking", "rustls-tls-webpki-roots"], optional = true }

[target.'cfg(unix)'.dependencies]
Comment thread
johntmyers marked this conversation as resolved.
nix = { workspace = true }

[features]
default = ["telemetry"]
## Compile in anonymous telemetry emission support. On by default; disable with
Expand Down
Loading
Loading