A zero-dependency, stdlib-only Go forward proxy that injects your real
credentials (GitHub GH_TOKEN, npm token, โฆ) into outbound requests on the
wire, so code running in a sandbox can use them without ever seeing them.
A simplified version of Infisical's agent-vault,
driven by a single script (sandbox.sh) over a dependency-free proxy binary you own.
๐ What it does
Code runs in a container whose HTTP(S) traffic goes through the proxy.
The proxy holds your tokens and injects them into outbound requests as they
leave โ so the workload can use them but never sees them. Other traffic
(raw TCP, ssh, ...) egresses directly by default; set SANDBOX_OPEN_NET=0
to make the proxy the only route out.
sandbox (git / gh / npm โ no token; HTTPS_PROXY, trusts proxy CA)
โ
โ request without credentials
โผ
sandbox-proxy (holds the real token; default-deny allow-list)
โ
โ + Authorization injected on the wire
โผ
upstream (github.com, api.github.com, registry.npmjs.org, โฆ)
- ๐ Open by default (
allow_all: true) for easy setup โ egress to any host, with credentials injected only on your configured hosts. Setallow_all: falsefor strict default-deny (only listed hosts reachable). - ๐ HTTPS interception via a CA it generates on first run and the sandbox
trusts; the intercepted TLS speaks HTTP/1.1 only (ALPN pins
http/1.1), and hosts you don't inject into can be blind-tunnelled untouched. - ๐ก๏ธ A compromised workload can at most use a token against the hosts you allow โ it can't read or exfiltrate the secret itself.
๐ Usage: the sandbox command
Source the control script once; it gives you a sandbox function that manages
one shared proxy and any number of sandbox containers. The function
re-sources sandbox.sh on every call, so edits to it take effect immediately โ
no need to source again (new default values of SANDBOX_* vars are the
exception: those stick per shell once set).
source sandbox.sh # GH_TOKEN is taken from `gh auth token` automatically if it isn't already set. export NPM_TOKEN=npm_xxx # optional; export any secret your rules need sandbox proxy up # build (if needed) + start the shared proxy sandbox proxy status # is it running? on which networks? # in any project directory: cd ~/code/my-app sandbox run # ensures proxy is up, opens a shell in $PWD # ...or run a command directly: sandbox run npm ci sandbox run git clone https://github.com/you/private.git
Inside a sandbox there is no token in the environment, yet git/npm are authenticated โ the proxy injects credentials on the way out. Run as many sandboxes as you like at once; they all share the single proxy:
(cd ~/code/app-a && sandbox run npm test) & (cd ~/code/app-b && sandbox run npm test) & sandbox ps # list running sandboxes
Commands:
| Command | Does |
|---|---|
sandbox proxy up |
Build if needed, start the shared proxy. |
sandbox proxy status |
Show whether it's running and its networks. |
sandbox proxy reload |
Restart the proxy, picking up current env/tokens and config edits. |
sandbox proxy down / logs |
Stop+remove / follow logs. |
sandbox run [cmd...] |
Ensure the proxy is up, run a sandbox in $PWD (shell if no cmd). |
sandbox build [proxy|box|all] |
Force-rebuild images. |
sandbox ps |
List running sandboxes. |
๐ Where secrets come from: for each var in $SANDBOX_SECRET_ENVS,
sandbox uses the environment value if set, otherwise runs
SANDBOX_<VAR>_CMD on the host. The default set:
GH_TOKEN # defaults to `gh auth token` โ being logged into gh is enough NPM_TOKEN SCW_SECRET_KEY FLY_API_TOKEN # defaults to `fly auth token` SCW_COCKPIT_TOKEN_<REGION> # one per Cockpit region (FR_PAR, NL_AMS, PL_WAW) โ tokens are region-scoped
Resolution happens in a subshell at proxy up/reload, so tokens never persist in your shell, and they're passed to the container by name (never on the command line). To pull another secret from a command, e.g.:
export SANDBOX_SECRET_ENVS="GH_TOKEN NPM_TOKEN AWS_TOKEN" export SANDBOX_AWS_TOKEN_CMD="aws-vault exec me -- printenv AWS_SESSION_TOKEN"
๐พ Persisting tool configs / caches: set SANDBOX_VOLUMES to a
whitespace-separated list of docker -v specs and every sandbox run mounts
them (named volumes are created on first use and survive across sandboxes):
export SANDBOX_VOLUMES="claude-config:/root/.claude codex-config:/root/.codex pi-config:/root/.pi"
Each entry is a raw -v spec, so host paths ($HOME/.foo:/root/.foo) and
read-only mounts (somevol:/root/.x:ro) work too. Note these are shared across
all sandboxes, so treat anything mounted there as readable by any workload.
๐ง Env vars in sandboxes: one owner per concern โ static env (proxy vars,
locale, placeholder tokens like GH_TOKEN=dummy) is image ENV in
sandbox/Dockerfile; per-run extras go in SANDBOX_ENVS, a whitespace-separated
list of docker -e specs (NAME=value, or bare NAME to forward from the
host) โ never real secrets, those belong in the proxy. Shell aliases and
interactive setup live in sandbox/zshrc.sandbox (baked into the image;
sandbox build box after edits).
โป๏ธ Changing tokens or rules: edit proxy/config.json and/or update the token
source, then sandbox proxy reload โ it re-resolves secrets (picking up a
rotated gh token) and restarts. The CA is stored in a persistent Docker
volume, so it survives reloads and sandboxes keep trusting it.
The sandbox network is created --internal. By default (SANDBOX_OPEN_NET=1)
each sandbox is also attached to the egress network, so traffic that doesn't
use the proxy (raw TCP, ssh, database connections, ...) reaches the internet
directly, while HTTP(S) keeps going through the proxy for credential injection.
Export SANDBOX_OPEN_NET=0 before sandbox run for strict isolation, where
the proxy is physically the only route out. Confirm:
sandbox run sh -c 'env | grep -i token' # -> nothing (no token inside the sandbox) sandbox run curl -sI https://example.com # reachable via the proxy (allow_all default); # with allow_all:false an unlisted host -> 403
Override defaults (network/image/volume names, which env vars are forwarded as
secrets) by exporting SANDBOX_* vars before sourcing โ see the top of
sandbox.sh.
๐ฆ Installing packages at runtime: the image sets both upper- and lower-case
proxy vars, so apt, curl, wget, go, npm, bun all route through the
proxy โ apt update && apt install <pkg> works (needs allow_all, or the Ubuntu
archive hosts allow-listed). These installs live in the --rm container and
vanish on exit; for anything you want every time, add it to sandbox/Dockerfile
and sandbox build box. Your host TERM/COLORTERM are forwarded too, so
full-color TUIs work.
๐ Reaching raw-TCP / LAN services: the proxy only speaks HTTP/HTTPS, but its
CONNECT/blind-tunnel is a generic TCP splice โ so any TCP service is reachable
via the bundled forward (alias tunnel) helper, which exposes it on
127.0.0.1 through the proxy:
forward 192.168.1.39 5432 # 127.0.0.1:5432 -> 192.168.1.39:5432 forward 192.168.1.39 5432 6379 & # several ports, backgrounded
Point your client at 127.0.0.1:<port>. Keep allow_all on and don't add a rule
for the host โ a rule would MITM it and break the raw stream. The proxy must be
able to reach the target on the LAN.
โ๏ธ Configuration
proxy/config.json maps secrets (how to build an auth header, with the value
read from the proxy's environment) to rules (which host gets which secret):
{
"allow_all": true,
"secrets": {
"github": { "type": "basic", "env": "GH_TOKEN", "username": "x-access-token" },
"github-api": { "type": "bearer", "env": "GH_TOKEN" },
"npm": { "type": "bearer", "env": "NPM_TOKEN" },
"scaleway": { "type": "header", "env": "SCW_SECRET_KEY", "header": "X-Auth-Token" }
},
"rules": [
{ "host": "github.com", "inject": "github" },
{ "host": "api.github.com", "inject": "github-api" },
{ "host": "*.githubusercontent.com" },
{ "host": "registry.npmjs.org", "inject": "npm" },
{ "host": "api.scaleway.com", "inject": "scaleway" }
]
}- secrets โ
typeisbearer,basic, orheader;envnames the host env var holding the token (never the value itself).usernameis for basic auth (GitHub uses the token as the password with any username).headersets the token in a custom header named byheaderโ e.g. Scaleway'sX-Auth-Token. - rules โ matched by
host, covering all methods and paths. A*.prefix is a suffix wildcard:*.githubusercontent.commatchesobjects./raw./ any subdomain (and the bare domain) โ handy for CDN hosts behind gh-release/git-lfs/npm-tarball downloads.injectnames a secret to add on every request; omit it to allow a host with no credential added. allow_allโtrueby default for simplicity: egress to any host, with injection still scoped to listed hosts (others are blind-tunnelled, untouched). Set it tofalsefor strict default-deny โ only listed hosts are reachable.โ ๏ธ Withallow_allon, the proxy is a credential broker, not a firewall: a compromised workload can send data anywhere. Flip it tofalsefor untrusted code.
Run sandbox proxy reload to re-read the config.
๐ฆ Run without Docker (portable binary)
The proxy is just a binary โ no runtime deps.
cd proxy/src go build -o sandbox-proxy . # or: GOOS=linux GOARCH=amd64 go build ... GH_TOKEN=ghp_xxx NPM_TOKEN=npm_xxx ./sandbox-proxy # listens on :3128, writes ca/ca.crt # point any client at it: export HTTPS_PROXY=http://127.0.0.1:3128 curl --cacert ca/ca.crt https://api.github.com/user # authenticated, token never left the proxy git -c http.proxy=$HTTPS_PROXY clone https://github.com/your/private.git
Cross-compile for any target with GOOS/GOARCH โ the output is a single
static file you can drop anywhere.
๐งช Tests
cd proxy/src && go test ./...
Covers the policy engine (host matching, decide, credential injection incl.
placeholder overwrite), config loading, header/host helpers, WebSocket-upgrade
detection, and end-to-end forward-proxy and CONNECT/MITM flows (injection,
default-deny, allow_all) via httptest. _test.go files are excluded from the
built binary.
๐ง Environment variables
| Var | Default | Meaning |
|---|---|---|
PROXY_LISTEN |
:3128 |
listen address |
PROXY_CONFIG |
config.json |
rules file |
PROXY_CA_DIR |
ca |
where ca.crt / ca.key are stored/generated |
HTTP(S)_PROXY |
โ | upstream proxy for the proxy's own egress (optional) |
๐ก๏ธ Security notes
ca.keystays in the proxy. It lives (mode0600) in the proxy-only CA volume; sandboxes bind-mount only the publicca.crtread-only, never the volume โ so a workload can't read the key and mint trusted certs. Keep the CA volume private on the host.- Lock down the network, not just the env. The
--internalsandbox network (sandbox-net) is what actually forces traffic through the proxy; without it a workload could ignoreHTTPS_PROXYand dial out directly. Note the defaultSANDBOX_OPEN_NET=1deliberately relaxes this by also attaching sandboxes to the egress network โ convenient for raw TCP, but a workload can then bypass the proxy entirely. ExportSANDBOX_OPEN_NET=0when you want enforced proxy-only egress. - Scope tightly. Prefer specific injection hosts over broad
allow_allso a leaked-but-injected token is useful only for what you intended.