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>@127.0.0.1:<port>);
curl/git/requests/… send it back as Proxy-Authorization: Basic base64(<token>:);
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>@127.0.0.1:<port>`); curl/git/requests/… send it back as `Proxy-Authorization: Basic base64(<token>:)`; 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.
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 |