Gateway-hosted loopback EGRESS PROXY — the one door a jailed shell child may use
to reach the network. Paired with the OS jail's net-off-except-loopback wall
(process-jail), it turns vis.yml :network (allowed/denied domains + verb/path
:rules) into REAL enforcement for shell children — curl, wget, a script,
subprocess — not a cooperative env-var hint.
How the wall + door fit together:
127.0.0.1:<this-port>, so a raw
socket() / /dev/tcp in a script has nowhere to go — the kernel, not
politeness, forces every byte through here.http_proxy/https_proxy/ALL_PROXY point the child's HTTP clients at this
port, so ordinary tools proxy voluntarily; the wall covers the rest.What it enforces (no MITM — no new deps, no CA):
CONNECT host:443): HOST allow/deny always; FULL method + path too when
a MITM capability is supplied and the policy asks for it (:mitm?) — the proxy
terminates the child's TLS with an ephemeral per-host leaf cert (see
internal.tls-mitm), reads the real verb/path, then re-encrypts to the real
upstream (whose real cert it still validates). Without MITM it is a raw byte
tunnel (verb opaque) — the documented CONNECT-only ceiling.The policy is a plain VALUE (per session), read fresh per connection via policy-fn
so /reload + config edits take effect with no restart. One request per upstream
connection (we force Connection: close), so HTTP keep-alive can't smuggle a second,
unfiltered verb onto an already-approved socket.
Gateway-hosted loopback EGRESS PROXY — the one door a jailed shell child may use
to reach the network. Paired with the OS jail's *net-off-except-loopback* wall
(`process-jail`), it turns vis.yml `:network` (allowed/denied domains + verb/path
`:rules`) into REAL enforcement for shell children — `curl`, `wget`, a script,
`subprocess` — not a cooperative env-var hint.
How the wall + door fit together:
- The jail denies ALL sockets except TCP to `127.0.0.1:<this-port>`, so a raw
`socket()` / `/dev/tcp` in a script has nowhere to go — the kernel, not
politeness, forces every byte through here.
- `http_proxy`/`https_proxy`/`ALL_PROXY` point the child's HTTP clients at this
port, so ordinary tools proxy voluntarily; the wall covers the rest.
What it enforces (no MITM — no new deps, no CA):
- Plain HTTP (absolute-form proxy request): FULL host + method + path, because
the request line is cleartext. GET-not-POST works here.
- HTTPS (`CONNECT host:443`): HOST allow/deny always; FULL method + path too when
a MITM capability is supplied and the policy asks for it (`:mitm?`) — the proxy
terminates the child's TLS with an ephemeral per-host leaf cert (see
`internal.tls-mitm`), reads the real verb/path, then re-encrypts to the real
upstream (whose real cert it still validates). Without MITM it is a raw byte
tunnel (verb opaque) — the documented CONNECT-only ceiling.
The policy is a plain VALUE (per session), read fresh per connection via `policy-fn`
so `/reload` + config edits take effect with no restart. One request per upstream
connection (we force `Connection: close`), so HTTP keep-alive can't smuggle a second,
unfiltered verb onto an already-approved socket.Gateway-lifecycle SANDBOX CAPABILITY: ONE shared loopback egress proxy and ONE ephemeral MITM CA for the WHOLE daemon, keyed PER SESSION.
Why shared, not per-session (turn 32): the gateway is multi-tenant — many clients/sessions hit one daemon. A per-session proxy+CA thrashes listeners and mints a fresh CA per session; worse, every child would carry a different trust root. Instead there is ONE listener and ONE CA (the "same certs" property — every child trusts the same root), plus a REGISTRY mapping a per-session TOKEN → that session's live policy fn.
How a connection is attributed to a session: the jailed child's proxy env carries
its unguessable token in the proxy URL userinfo
(http://<token>:vis@127.0.0.1:<port>); curl/git/requests/… send it back as
Proxy-Authorization: Basic base64(<token>:vis) and the password is ignored;
the proxy hands the token to resolve-policy, which looks up the registry.
FAIL-CLOSED: a request whose token is missing or not registered to a LIVE session
is DENIED (a :deny-all? sentinel policy) — the shared door never serves a policy
it cannot attribute. The token is a random UUID, so one session cannot reach
another's (broader) policy by guessing.
Lazy: the proxy listener and the CA keygen happen only on first ensure-proxy! /
ensure-ca! — a gateway that never jails a shell child opens neither.
Gateway-lifecycle SANDBOX CAPABILITY: ONE shared loopback egress proxy and ONE ephemeral MITM CA for the WHOLE daemon, keyed PER SESSION. Why shared, not per-session (turn 32): the gateway is multi-tenant — many clients/sessions hit one daemon. A per-session proxy+CA thrashes listeners and mints a fresh CA per session; worse, every child would carry a different trust root. Instead there is ONE listener and ONE CA (the "same certs" property — every child trusts the same root), plus a REGISTRY mapping a per-session TOKEN → that session's live policy fn. How a connection is attributed to a session: the jailed child's proxy env carries its unguessable token in the proxy URL userinfo (`http://<token>:vis@127.0.0.1:<port>`); curl/git/requests/… send it back as `Proxy-Authorization: Basic base64(<token>:vis)` and the password is ignored; the proxy hands the token to `resolve-policy`, which looks up the registry. FAIL-CLOSED: a request whose token is missing or not registered to a LIVE session is DENIED (a `:deny-all?` sentinel policy) — the shared door never serves a policy it cannot attribute. The token is a random UUID, so one session cannot reach another's (broader) policy by guessing. Lazy: the proxy listener and the CA keygen happen only on first `ensure-proxy!` / `ensure-ca!` — a gateway that never jails a shell child opens neither.
OS-level process CONTAINMENT — the 'jail' — that wraps the shell executors'
argv so an allowed child is physically confined to the session workspace roots
and, when network is off, cannot open a socket. This is a real containment
boundary — not a cooperative name/argv check, which can be walked around since
argv[0] is bash and the real binary hides inside the -lc string; the jail
constrains what the child can DO once it runs, regardless of what a script
inside it tries (curl, python -c, /dev/tcp — all hit the same wall).
POLICY, NOT GUARDS. The jail is driven by a declarative policy compiled from vis.yml + the LIVE session roots, not by hand-written guard functions. The policy is a plain VALUE passed per spawn (never a process-global singleton, so many concurrent sessions in one gateway never stomp each other). Its shape:
{:roots-fn (fn [] [root-strings]) ; live session RW roots, re-read/spawn
:net-enabled? <bool> ; whole shell-child network on/off
:allow-read-write [<path> …] ; full read+write grant
:deny-write [<path> …] ; protect within writable (deny wins)
:allow-read [<path> …] ; additional read-only paths
:deny-read [<path> …] ; protect a read region (deny wins)
:deny-exec [<path> …] ; readable but never executable
:keychain? <bool> ; the OS credential store is reachable
:inbound-ports [<int> …] ; ports a child may ACCEPT on from
; other hosts (loopback is always open)
:env-values {<NAME> <value>} ; RESOLVED project env (.env +
; environment:) with ONE call's own
; env delta merged on, per spawn
:env-removals #{<NAME> …} ; names THAT call asked to UNSET
:inherit-host-env? <bool>} ; jail.environment: inherit — the child
; also keeps the operator's ambient env
The filesystem model mirrors Anthropic's sandbox-runtime:
:allow-read-write; :deny-write wins.:allow-read
are readable; :deny-read wins.This namespace owns WHAT a session's child may do: it turns the session's
configuration, live roots, proxy endpoint and call environment into one
platform-neutral policy value and the complete child environment. HOW the
operating system enforces that value belongs to com.blockether/vis-python-runtime
(spawn-process! with :policy): the per-platform enforcement, the
already-confined marker and the refusal on a host that cannot enforce all live
there, beside the process launcher, so no enforcement text is assembled here.
OS-level process CONTAINMENT — the 'jail' — that wraps the shell executors'
argv so an allowed child is physically confined to the session workspace roots
and, when network is off, cannot open a socket. This is a real containment
boundary — not a cooperative name/argv check, which can be walked around since
argv[0] is `bash` and the real binary hides inside the `-lc` string; the jail
constrains what the child can DO once it runs, regardless of what a script
inside it tries (curl, python -c, /dev/tcp — all hit the same wall).
POLICY, NOT GUARDS. The jail is driven by a declarative *policy* compiled from
vis.yml + the LIVE session roots, not by hand-written guard functions. The
policy is a plain VALUE passed per spawn (never a process-global singleton, so
many concurrent sessions in one gateway never stomp each other). Its shape:
{:roots-fn (fn [] [root-strings]) ; live session RW roots, re-read/spawn
:net-enabled? <bool> ; whole shell-child network on/off
:allow-read-write [<path> …] ; full read+write grant
:deny-write [<path> …] ; protect within writable (deny wins)
:allow-read [<path> …] ; additional read-only paths
:deny-read [<path> …] ; protect a read region (deny wins)
:deny-exec [<path> …] ; readable but never executable
:keychain? <bool> ; the OS credential store is reachable
:inbound-ports [<int> …] ; ports a child may ACCEPT on from
; other hosts (loopback is always open)
:env-values {<NAME> <value>} ; RESOLVED project env (`.env` +
; `environment:`) with ONE call's own
; `env` delta merged on, per spawn
:env-removals #{<NAME> …} ; names THAT call asked to UNSET
:inherit-host-env? <bool>} ; `jail.environment: inherit` — the child
; also keeps the operator's ambient env
The filesystem model mirrors Anthropic's sandbox-runtime:
- WRITE is allow-only: denied everywhere except the session roots + tmp +
`:allow-read-write`; `:deny-write` wins.
- READ is default-deny here (workspace-focused, stronger than srt's
read-everywhere default): system code/config + RW paths + `:allow-read`
are readable; `:deny-read` wins.
This namespace owns WHAT a session's child may do: it turns the session's
configuration, live roots, proxy endpoint and call environment into one
platform-neutral policy value and the complete child environment. HOW the
operating system enforces that value belongs to `com.blockether/vis-python-runtime`
(`spawn-process!` with `:policy`): the per-platform enforcement, the
already-confined marker and the refusal on a host that cannot enforce all live
there, beside the process launcher, so no enforcement text is assembled here.Canonical immutable security-policy snapshots and their model-facing view.
A snapshot is created once for a root environment, inherited unchanged by child environments, and replaced only by an explicit environment rebuild. Enforcement and context both derive from this value.
Canonical immutable security-policy snapshots and their model-facing view. A snapshot is created once for a root environment, inherited unchanged by child environments, and replaced only by an explicit environment rebuild. Enforcement and context both derive from this value.
Scoped access settings. Local policy can narrow, never expand, the host ceiling.
Scoped access settings. Local policy can narrow, never expand, the host ceiling.
Ephemeral CA + per-host leaf minting for the egress proxy's TLS-terminating (MITM) tier — the piece that gives a jailed shell child GET-not-POST fidelity over HTTPS, matching what the interpreter method-guard already does pre-TLS.
Why it exists: a plain CONNECT proxy only sees CONNECT host:443 — the method
and path live inside the TLS the proxy never opens, so HTTPS verb/path is
opaque. To read them the proxy must TERMINATE the child's TLS: present the
child a leaf cert for the requested host, decrypt, inspect method+path, then
re-encrypt to the real upstream (whose real cert the proxy still validates).
Trust model:
CURL_CA_BUNDLE/SSL_CERT_FILE
/REQUESTS_CA_BUNDLE/NODE_EXTRA_CA_CERTS/GIT_SSL_CAINFO). Only children
inside the jail ever see or trust it.No JCA provider is registered globally: bcpkix's Jca* builders use the default platform signer (SHA256withRSA via SunRsaSign), which keeps this native-image friendly and side-effect free.
Ephemeral CA + per-host leaf minting for the egress proxy's TLS-terminating
(MITM) tier — the piece that gives a jailed shell child GET-not-POST fidelity
over HTTPS, matching what the interpreter method-guard already does pre-TLS.
Why it exists: a plain CONNECT proxy only sees `CONNECT host:443` — the method
and path live inside the TLS the proxy never opens, so HTTPS verb/path is
opaque. To read them the proxy must TERMINATE the child's TLS: present the
child a leaf cert for the requested host, decrypt, inspect method+path, then
re-encrypt to the real upstream (whose real cert the proxy still validates).
Trust model:
- The CA is EPHEMERAL and per-session — born in this JVM, never written to
the host trust store. Its cert PEM is written to a temp file whose path is
injected into the jailed child's trust env (`CURL_CA_BUNDLE`/`SSL_CERT_FILE`
/`REQUESTS_CA_BUNDLE`/`NODE_EXTRA_CA_CERTS`/`GIT_SSL_CAINFO`). Only children
inside the jail ever see or trust it.
- Upstream (proxy -> real server) uses the SYSTEM trust store by default, so
the real server's real certificate is still validated end to end.
No JCA provider is registered globally: bcpkix's Jca* builders use the default
platform signer (SHA256withRSA via SunRsaSign), which keeps this native-image
friendly and side-effect free.cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |