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.
Code runs in a container with no network access except 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.
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: false
for strict default-deny (only listed hosts reachable). - 🔏
HTTPS interception via a CA it generates on first run and the sandbox trusts; the intercepted TLS speaksHTTP/1.1 only(ALPN pinshttp/1.1
), and hosts you don't inject into can be blind-tunnelled untouched. - 🛡️ A compromised workload can at most usea token against the hosts you allow — it can't read or exfiltrate the secret itself.
Source the control script once; it gives you a sandbox
function that manages one shared proxy and any number of sandbox containers.
source sandbox.sh
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?
cd ~/code/my-app
sandbox run # ensures proxy is up, opens a shell in $PWD
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 |
| Force-rebuild images. | |
sandbox ps |
|
| List running sandboxes. |
🔑 Where secrets come from: for each var in $SANDBOX_SECRET_ENVS
(default
GH_TOKEN NPM_TOKEN
), sandbox
uses the environment value if set, otherwise
runs SANDBOX_<VAR>_CMD
on the host. GH_TOKEN
defaults to gh auth token
, so
just being logged into gh
is enough — no need to export anything. 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.
♻️ 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
, so a sandbox physically cannot reach the internet except through the proxy. 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);
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.
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" }
},
"rules": [
{ "host": "github.com", "inject": "github" },
{ "host": "api.github.com", "inject": "github-api" },
{ "host": "*.githubusercontent.com" },
{ "host": "registry.npmjs.org", "inject": "npm" }
]
}
secrets—type
isbearer
orbasic
;env
names the host env var holding the token (never the value itself).username
is for basic auth (GitHub uses the token as thepasswordwith any username).rules— matched byhost
, covering all methods and paths. A*.
prefix is a suffix wildcard:*.githubusercontent.com
matchesobjects.
/raw.
/ any subdomain (and the bare domain) — handy for CDN hosts behind gh-release/git-lfs/npm-tarball downloads.inject
names a secret to add on every request; omit it to allow a host with no credential added.—allow_all
for simplicity: egress to any host, with injection still scoped to listed hosts (others are blind-tunnelled, untouched). Set it totrue
by defaultfalse
for strict default-deny — only listed hosts are reachable.⚠️ Withallow_all
on, the proxy is a credentialbroker, not a firewall: a compromised workload can send data anywhere. Flip it tofalse
for untrusted code.
Run sandbox proxy reload
to re-read the config.
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
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.
cd proxy/src && go test ./...
Covers the policy engine (host matching, decide
, credential injection incl.
placeholder overwrite), config , 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.
| 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) |
It lives (modeca.key
stays in the proxy.0600
) in the proxy-only CA volume; sandboxes bind-mountonly the publicca.crt
read-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--internal
sandbox network (sandbox-net
) is what actually forces traffic through the proxy; without it a workload could ignoreHTTPS_PROXY
and dial out directly.sandbox.sh
creates it internal by default.Scope tightly. Prefer specific injection hosts over broadallow_all
so a leaked-but-injected token is useful only for what you intended.