Slicer Proxy — filtered egress and secret injection for microVMs
Slicer Proxy is a daemon that runs alongside Slicer and becomes the only egress path for microVMs. Use it to:
- Audit and log every outbound HTTP(S) request from a VM
- Default-deny egress and allow only specific hosts / paths / methods / ports
- Inject credentials (Bearer, Basic, OAuth) so the VM never sees the real secret
- Expire rules automatically with a TTL
- Passthrough raw TCP (SSH, Postgres, cert-pinned clients) without TLS termination
This skill assumes a running Slicer daemon — see the use-slicer skill for connecting to one. Full docs: https://docs.slicervm.com/proxy/
Concepts
Slicer Proxy separates three nouns:
- client — a name plus a token. The VM authenticates to the proxy with that token (carried in
HTTPS_PROXY), which is how the proxy identifies it — no source-IP guessing. - secret — a named upstream credential (
bearer,basic, or anoauth-*type). The value lives on the proxy, never in the VM. - allow rule — owned by a client: a
hostplus optionalpaths/methods/ports/ttl, and optionally a referencedsecret.
Default deny. A client with no rules reaches nothing. An allow rule may be bare (just open a host) or reference a secret (inject a credential when it matches).
Ports and bind address
:3128— plaintext listener →HTTP_PROXY=http://:<token>@<ip>:3128:3129— TLS listener →HTTPS_PROXY=https://proxy:<token>@<ip>:3129- Proxy IP: Linux defaults to
192.168.222.1(set with--bind); macOS is192.168.64.1(the NAT gateway).
The :3129 outer certificate is signed by the host group CA, so a guest that already trusts the Slicer CA validates it with no extra flags.
Setup on Linux
Slicer Proxy only makes sense with isolated networking — the only mode where the proxy is the sole egress path. In bridge mode the VM has direct NAT'd Internet and bypasses the proxy entirely, so rules do nothing.
- Generate an isolated config that drops all egress except the proxy ports:
slicer new sbox --count=0 \
--net=isolated \
--drop=0.0.0.0/0 \
--allow=192.168.222.1:3128 \
--allow=192.168.222.1:3129 \
--find-ssh-keys=false \
--ca \
--socket ./slicer.sock \
> slicer.yaml
- Pre-generate the CA for the host group (writes
./.slicer/ca/sbox/):
sudo slicer ca init --hostgroup sbox
- Start the proxy (terminal 1). On Linux a concrete
--bindIPv4 auto-creates a per-IP dummy adapter, which needssudo(CAP_NET_ADMIN):
sudo slicer proxy up \
--hostgroup sbox \
--bind 192.168.222.1 \
--deny-cidr 192.168.1.0/24
--deny-cidr blocks the proxy from dialing ranges after DNS resolution — always exclude your LAN range (consider 127.0.0.1/8 too). Deny-CIDRs win over any client allow rule.
- Start Slicer (terminal 2).
slicer.yamlis the default name, so no argument is needed:
sudo slicer up
Setup on macOS
macOS support differs enough that this skill defers the exact steps to the docs — follow https://docs.slicervm.com/proxy/mac/ and trust those instructions.
Key differences from Linux:
- Two host groups.
slicerholds one long-lived VM — it can audit and inject secrets but cannot block egress.sboxholds on-demand sandbox VMs and can be forced fully through the proxy. - Egress blocking is off by default. Forcing traffic through the proxy is opt-in. Edit
~/slicer-mac/slicer-mac.yamlon thesboxhost group — setca: { generate: true },network.dns_servers: ["127.0.0.1","127.0.0.1"], andnetwork.allow/network.drop— then apply the host firewall rules withsudo ~/slicer-mac/slicer-mac pf apply(revert withpf remove). Without these editssboxVMs keep full Internet access and the proxy only sees traffic that opts in. These network settings apply to everysboxVM; use separate proxy clients and rules when individual VMs need different policies. The docs page has the exact YAML diff. - Fixed proxy IP. macOS uses the NAT gateway
192.168.64.1— it is not configurable as it is on Linux. - Start flags. Run
slicer proxy upfrom~/slicer-macwith--bind 0.0.0.0 --san 192.168.64.1 --seal-key-file ./.slicer/proxy/mk; start Slicer withslicer-mac up.
Once set up, the client / secret / allow-rule workflow below is identical to Linux — use 192.168.64.1 as the proxy IP and drop sudo from the slicer commands.
For a cold-fork workflow, use an open client only while preparing the hot builder, then use a separate default-deny or restricted client for each cold runner. Do not bake the builder client token into the committed disk. macOS cannot vary host firewall rules per fork as Linux can with an isolated network namespace; the proxy client token is the per-VM policy selector.
Core workflow
Register a client, grant it a host, launch a VM with the client token, and watch egress flow only through the proxy.
export SLICER_URL="./slicer.sock" # or your daemon URL
# 1. Create a client; capture its token (printed bare on stdout)
PROXY_TOKEN=$(slicer proxy client create web-1)
# 2. Allow it to reach a host (everything else is denied)
sudo slicer proxy allow web-1 --host wikipedia.org
# 3. Launch a VM
slicer vm launch --tag role=web-1
# 4. Without the token: egress is blocked (DNS fails fast)
sudo slicer vm exec sbox-1 -- curl -sS --max-time 3 https://wikipedia.org
# 5. With the token in HTTPS_PROXY, egress flows through the proxy
sudo slicer vm exec \
--env HTTPS_PROXY="https://proxy:$PROXY_TOKEN@192.168.222.1:3129" \
sbox-1 -- curl -iS https://wikipedia.org
On macOS, drop sudo and use 192.168.64.1.
Step 4 cannot even resolve DNS — the VM has no egress without the proxy. Step 5 returns HTTP/2 301 (Wikipedia redirecting to www.wikipedia.org), proving the request reached the Internet through the proxy. Note www.wikipedia.org is a different host: following that redirect needs its own allow rule. Hostnames match exactly, or by wildcard (*.wikipedia.org).
Manage clients with slicer proxy client list and slicer proxy client delete <name> (deleting a client revokes its token and drops its rules).
Allow rules
# Open a whole host (default ports 80 + 443)
slicer proxy allow web-1 --host archive.ubuntu.com
# Narrow by method + path (exact, or suffix-glob like '/system/*')
slicer proxy allow web-1 --host archive.ubuntu.com --method GET --path '/ubuntu/*'
# Narrow by upstream port
slicer proxy allow web-1 --host db.example.com --port 5432
# Allow every host (audit-style)
slicer proxy allow web-1 --host '*'
# Expire automatically after 1 hour
slicer proxy allow web-1 --host api.example.com --ttl 1h
Rules are first-match-wins, in declaration order — add the narrowest rule first. Multiple rules per host are allowed when methods or paths differ. --host accepts an exact name, a wildcard (*.github.com), or * for all.
Inspect and remove rules:
slicer proxy rules web-1 # list in declaration order (--json for raw)
slicer proxy revoke web-1 --host api.example.com # bulk: every rule for that host
slicer proxy revoke web-1 --host api.example.com --method GET --path '/x' # surgical: one rule
revoke is surgical when you repeat the flags used at create time, bulk-by-host otherwise.
Secret injection
The proxy can attach a credential to matching requests so the VM never holds the real value.
# 1. Register the upstream credential on the proxy (value stays on the host)
slicer proxy secret create llm-key --host llama.example.com \
--type bearer --value-file ./llm-token.txt
# 2. Reference it from an allow rule
slicer proxy allow web-1 --host llama.example.com --secret llm-key
Now a VM request to https://llama.example.com/... sent without an Authorization header has Authorization: Bearer <secret> injected by the proxy. The client's own Authorization header, if any, is stripped first.
Secret types (--type):
| Type | Value | Use |
|---|---|---|
bearer (default) |
token via --value-file |
Authorization: Bearer <value> |
basic |
user:pass via --value |
HTTP Basic auth |
oauth-claude |
--value-file ~/.claude/.credentials.json |
Claude Code — proxy refreshes the token |
oauth-codex |
--value-file ~/.codex/auth.json |
Codex / ChatGPT login |
oauth-github-copilot |
--value-file ~/.local/share/opencode/auth.json |
GitHub Copilot |
oauth-xai |
--value-file from slicer proxy oauth xai |
xAI Grok |
OAuth credentials are adopted — obtained on the host, handed to the proxy, then refreshed by the proxy — rather than injected mid-flow, so the VM can never capture a real token. Prefer --value-file over --value (which lands in shell history). Re-adopt after a fresh host-side login with --force.
For xAI, run the loopback login first:
slicer proxy oauth xai > ./xai-oauth.json
slicer proxy secret create xai --host api.x.ai --type oauth-xai --value-file ./xai-oauth.json
List and delete secrets with slicer proxy secret list / slicer proxy secret delete <name> (values are never returned).
Audit mode — discover the paths a workload needs
Start the proxy with --mode=audit to log the method + path of denied HTTPS requests. It MITMs unknown TLS far enough to read the first inner request, logs it, then returns 403 without forwarding upstream:
sudo slicer proxy up --hostgroup sbox --bind 192.168.222.1 \
--deny-cidr 192.168.1.0/24 --mode=audit
Log lines look like:
deny client=web-1 method=GET scheme=https host=api.example.com port=443 path=/v1/models mode=audit reason=no-rule
Use the output to write precise allow rules, then restart the proxy in the default strict mode for enforcement. Audit mode relies on the guest trusting the Slicer Proxy CA; pinned-cert clients fail closed (host-only logging).
Passthrough — raw TCP (SSH, Postgres, pinned certs)
--passthrough splices TCP at CONNECT time without terminating TLS. Required for cert-pinned clients and non-HTTP protocols. It is mutually exclusive with --secret / --method / --path — the audit log carries only host, port, byte counts, and duration.
slicer proxy allow build-1 --host db.internal.example.com --port 5432 --passthrough
slicer proxy allow build-1 --host bastion.example.com --port 22 --passthrough
For SSH, set a ProxyCommand in ~/.ssh/config on the host:
Host bastion
ProxyCommand nc -x 192.168.222.1:3129 %h %p # macOS: 192.168.64.1
Explicit proxying when guest DNS is blocked
An isolated guest may be unable to reach the public resolver in
/etc/resolv.conf. That does not require transparent proxy installation for
HTTP clients. Point the client at slicer-proxy by IP address: it carries
the target hostname in the absolute HTTP request or CONNECT target, and the
proxy resolves that hostname upstream.
For Docker and BuildKit, install the CA with slicer-agent ca install, then
set HTTP_PROXY, HTTPS_PROXY, and NO_PROXY in the daemon and builder
configuration. For APT, put an IP-addressed explicit proxy in a root-readable
file under /etc/apt/apt.conf.d/:
Acquire::http::Proxy "http://:TOKEN@192.168.222.1:3128";
Acquire::https::Proxy "https://proxy:TOKEN@192.168.222.1:3129";
Install the CA before using the TLS listener, keep the token out of logs, and remove temporary credential files when finished. Do not briefly install and remove the transparent helper merely to make APT resolve repository names.
Transparent proxy helper
When setting HTTP(S)_PROXY per command is awkward, the in-VM helper redirects egress automatically with iptables. It needs the regular (non-min) image and the VM's DNS set to 127.0.0.1.
# Inside the VM, as root — point it at the proxy with the client token
sudo slicer-agent proxy install 192.168.222.1 --token "$PROXY_TOKEN"
proxy install always manages OUTPUT redirects for TCP 80 and 443. Its
--dns flag only toggles the additional local DNS listener; it is not a
DNS-only mode. There is currently no supported DNS-only installation. If the
workload supports an explicit proxy, use the explicit configuration above
instead of fighting the helper's managed iptables rules.
It also supports per-port TCP tunnels and an SSH ProxyCommand (the destination needs a --passthrough allow rule):
# In the VM:
sudo slicer-agent proxy tunnel add pg --listen 127.0.0.1:5432 db.internal:5432
ssh -o ProxyCommand='slicer-agent proxy connect %h:%p' user@bastion
See https://docs.slicervm.com/proxy/transparent/ for the helper, tunnel management, and Docker build caveats (containers need the CA at /runner/ca.crt added to their trust store).
Troubleshooting: connections reach nothing, or reach it silently
Symptom: curl -x http://<proxy-ip>:3128/ ... (or the guest equivalent) gets
curl: (52) Empty reply from server / curl: (56) Proxy CONNECT aborted, the
same request via 127.0.0.1 on the same proxy works fine, and the proxy's own
log shows nothing at all for the failing request. This exact signature took a
multi-hour, multi-theory investigation to resolve once (a customer's macOS
Application Firewall was set to block all incoming connections) — use this
order to get there in minutes instead:
- Reach for
--tracefirst, before any other theory.slicer proxy up --tracedrops the logger to Debug and logs every stage of the CONNECT path:connection accepted(raw TCP accept, before any HTTP parsing) →request received→authenticated→rule matched→egress check passed→hijacked connection→wrote CONNECT 200→issued MITM leaf→inner TLS handshake ok→entering MITM tunnel. Re-run the exact failing request. If nothing prints, not evenconnection accepted, the request never reaches this process at all — stop looking at slicer/proxy config and rules entirely; the cause is upstream of the Go process. - Loopback works, the bind/gateway IP doesn't, on both host- and
guest-originated traffic → this is not a slicer bug. It's the signature
of something intercepting non-loopback traffic before it reaches the
listener: a host-level firewall or a network content-filter extension.
On macOS check, in this order:
/usr/libexec/ApplicationFirewall/socketfilterfw --getblockall— if it reportsenabled, that's very likely the whole answer. Disable it, or explicitly allow theslicerbinary under System Settings → Network → Firewall → Options.systemextensionsctl listfor anyNEFilterProvider/content-inspection extension (Little Snitch, corporate EDR/AV like Bitdefender). Note that toggling one of these "off" in its own app does not reliably unload the underlying extension — a[activated disabled]extension can still intercept traffic. A full uninstall via the vendor's own uninstaller is the only fully conclusive test.
- If you need to prove it at the wire level, capture on the bridge
interface during the exact failing request (macOS:
bridge100; find the right one via the gateway's ownifconfig):
A completed handshake plus an ACKed request, followed by FIN+RST with the response side's sequence number never advancing past 1 (i.e. zero response bytes ever written), combined with no trace log at all for that request, is conclusive: something completed the TCP handshake on behalf of the bind address and closed it — the real process'ssudo tcpdump -i bridge100 -n host <gateway-ip> and port 3128 -w /tmp/cap.pcapAccept()was never called. Don't stop at "the handshake completed" — a handshake and a byte-level ACK happen entirely in the kernel/network-extension layer and prove nothing about whether the application ever saw the connection. - What this is not — don't waste a round-trip re-checking these once
--tracealready stayed silent:- pf state.
slicer-mac pf apply/pf removerules are scoped to thesboxguest source range only (.3and up) — they never match host-originated traffic, and cannot explain a host curl failing. - CA / cert trust. A CA mismatch fails after a successful CONNECT —
curl gets
HTTP/1.1 200 Connection establishedfirst, then fails the inner TLS handshake (curl exit60,SSL certificate problem). It cannot produce an empty reply or an aborted CONNECT (exit52/56), because the CONNECT response is plain text, written before any certificate logic runs. - Seal/mk key mismatch. This fails loudly, at startup —
Open sealed proxy state ...: unwrap data key: cipher: message authentication failed— and the process never reachesHTTP listening. It cannot present as a running proxy that silently drops one request; if the proxy started and loggedHTTP listening/HTTPS listening, the seal key is fine. - A stale/duplicate process on the port. Cheap to check
(
lsof -nP -iTCP:3128 -sTCP:LISTEN) but don't assume it without checking — a single PID owning both ports is common and doesn't rule out the above; it just means one process, not several, is (or isn't) seeing the traffic.
- pf state.
- Isolating port number vs. protocol vs. destination, if none of the
above resolves it:
--http-port/--https-portare freely reassignable. Test the identical plaintext request on a random high port (isolates "is 3128 specifically flagged as a known proxy port") separately from testing the TLS listener (isolates "is it plaintext content inspection specifically"). Change one variable at a time.
Notes
- Linux: isolated mode only. In bridge mode VMs have direct NAT egress and skip the proxy — rules then do nothing.
- The proxy is default-deny, and so is each client. Nothing leaves a VM until a client and an allow rule exist.
slicer proxyrequires a recent Slicer build — runslicer proxy --helpto confirm it is present.- Reset the CA: stop the daemons,
sudo rm -rf .slicer/ca/<hostgroup> proxy.crt proxy.key, then re-runslicer ca init.